@lensagent/core 0.1.0 → 0.1.2
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 +63 -4
- package/config/lens.example.jsonc +36 -13
- package/dist/adapters/executor-router.d.ts +20 -0
- package/dist/adapters/executor-router.js +86 -0
- package/dist/adapters/executor.d.ts +9 -3
- package/dist/adapters/executor.js +196 -61
- package/dist/adapters/lsp.d.ts +2 -1
- package/dist/adapters/lsp.js +98 -41
- package/dist/adapters/mcp.js +52 -17
- package/dist/adapters/revert.d.ts +5 -4
- package/dist/adapters/revert.js +6 -64
- package/dist/adapters/web.d.ts +9 -0
- package/dist/adapters/web.js +244 -0
- package/dist/config/files.d.ts +14 -0
- package/dist/config/files.js +57 -0
- package/dist/config/runtime.d.ts +3 -4
- package/dist/config/runtime.js +116 -97
- package/dist/config/schema.d.ts +56 -10
- package/dist/config/schema.js +48 -8
- package/dist/contracts/capability.d.ts +27 -1
- package/dist/contracts/domain.d.ts +1 -0
- package/dist/contracts/model.d.ts +33 -6
- package/dist/contracts/review.d.ts +17 -0
- package/dist/contracts/review.js +1 -0
- package/dist/core/access-policy.d.ts +40 -0
- package/dist/core/access-policy.js +68 -0
- package/dist/core/access.d.ts +30 -2
- package/dist/core/access.js +159 -26
- package/dist/core/context-boundaries.d.ts +25 -0
- package/dist/core/context-boundaries.js +106 -0
- package/dist/core/context.d.ts +3 -0
- package/dist/core/context.js +11 -0
- package/dist/core/host.js +261 -173
- package/dist/entry/pair.d.ts +1 -0
- package/dist/entry/pair.js +14 -0
- package/dist/entry/run.js +1 -1
- package/dist/execution/lsp-process.d.ts +3 -2
- package/dist/execution/lsp-process.js +46 -28
- package/dist/http/server.d.ts +5 -0
- package/dist/http/server.js +262 -80
- package/dist/runtime/agent.d.ts +9 -4
- package/dist/runtime/agent.js +361 -286
- package/dist/runtime/artifacts.d.ts +13 -1
- package/dist/runtime/artifacts.js +137 -10
- package/dist/runtime/cache.d.ts +59 -16
- package/dist/runtime/cache.js +124 -43
- package/dist/runtime/calls.d.ts +1 -1
- package/dist/runtime/calls.js +164 -48
- package/dist/runtime/capabilities.d.ts +2 -2
- package/dist/runtime/capabilities.js +4 -5
- package/dist/runtime/cleanup.d.ts +5 -2
- package/dist/runtime/cleanup.js +48 -17
- package/dist/runtime/compaction.d.ts +9 -0
- package/dist/runtime/compaction.js +52 -0
- package/dist/runtime/context.d.ts +8 -4
- package/dist/runtime/context.js +115 -128
- package/dist/runtime/events.d.ts +7 -0
- package/dist/runtime/events.js +83 -0
- package/dist/runtime/images.d.ts +14 -0
- package/dist/runtime/images.js +82 -0
- package/dist/runtime/kernel-capabilities.d.ts +7 -1
- package/dist/runtime/kernel-capabilities.js +146 -68
- package/dist/runtime/model-audit.d.ts +9 -0
- package/dist/runtime/model-audit.js +39 -0
- package/dist/runtime/model.d.ts +109 -4
- package/dist/runtime/model.js +368 -143
- package/dist/runtime/observability.d.ts +1 -0
- package/dist/runtime/observability.js +60 -15
- package/dist/runtime/policy.d.ts +1 -1
- package/dist/runtime/policy.js +2 -2
- package/dist/runtime/profile.d.ts +2 -1
- package/dist/runtime/profile.js +12 -15
- package/dist/runtime/projects.d.ts +3 -1
- package/dist/runtime/projects.js +38 -17
- package/dist/runtime/ptc-worker.d.ts +1 -0
- package/dist/runtime/ptc-worker.js +97 -0
- package/dist/runtime/ptc.d.ts +16 -0
- package/dist/runtime/ptc.js +128 -0
- package/dist/runtime/results.d.ts +5 -4
- package/dist/runtime/results.js +29 -51
- package/dist/runtime/review.d.ts +4 -1
- package/dist/runtime/review.js +101 -22
- package/dist/runtime/scheduler.js +1 -0
- package/dist/runtime/skills.js +34 -5
- package/dist/runtime/task-audit.d.ts +21 -0
- package/dist/runtime/task-audit.js +122 -0
- package/dist/runtime/time.d.ts +9 -0
- package/dist/runtime/time.js +18 -0
- package/dist/runtime/tokenizer.d.ts +2 -0
- package/dist/runtime/tokenizer.js +26 -1
- package/dist/store/database.js +26 -1
- package/dist/store/event-log.d.ts +1 -0
- package/dist/store/event-log.js +22 -5
- package/dist/store/schema.d.ts +1 -1
- package/dist/store/schema.js +50 -1
- package/dist/store/store.d.ts +65 -19
- package/dist/store/store.js +986 -188
- package/package.json +18 -8
- package/proto/executor.proto +69 -55
package/dist/runtime/model.js
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
|
+
import { assertContextBoundaries } from '../core/context-boundaries.js';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { sha256 } from './model-audit.js';
|
|
1
4
|
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
2
5
|
import { createOpenAI } from '@ai-sdk/openai';
|
|
3
6
|
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
4
|
-
import { jsonSchema,
|
|
5
|
-
import {
|
|
7
|
+
import { jsonSchema, streamText, tool } from 'ai';
|
|
8
|
+
import { canonicalize } from 'json-canonicalize';
|
|
9
|
+
import { digest, jsonValue, toolArguments } from '../core/codec.js';
|
|
6
10
|
import { fail } from '../core/domain.js';
|
|
7
11
|
import { resolveSecret } from '../config/runtime.js';
|
|
8
|
-
import { resolveProfile } from './profile.js';
|
|
12
|
+
import { resolveProfile, resolveReasoningEffort } from './profile.js';
|
|
13
|
+
import { record, selectCacheBreakpoints } from './cache.js';
|
|
14
|
+
import { estimateTokens } from './tokenizer.js';
|
|
15
|
+
import { imageMessages, imageReference, imageTokenEstimate, materializeImages } from './images.js';
|
|
9
16
|
import { assertProviderManifest, providerManifest } from './manifests.js';
|
|
10
17
|
export class AiModelGateway {
|
|
11
18
|
config;
|
|
12
19
|
target;
|
|
13
|
-
|
|
20
|
+
artifacts;
|
|
21
|
+
audit;
|
|
22
|
+
constructor(config, target, artifacts, audit) {
|
|
14
23
|
this.config = config;
|
|
15
24
|
this.target = target;
|
|
25
|
+
this.artifacts = artifacts;
|
|
26
|
+
this.audit = audit;
|
|
16
27
|
}
|
|
17
28
|
resolve(reference, config = this.config.current.config) {
|
|
18
29
|
const { providerId, modelId } = splitReference(reference);
|
|
@@ -26,177 +37,363 @@ export class AiModelGateway {
|
|
|
26
37
|
contextWindowTokens: model.contextWindowTokens,
|
|
27
38
|
profile: resolveProfile(config, model.harnessProfile),
|
|
28
39
|
usageReliable: model.usageReliable ?? false,
|
|
29
|
-
cacheSupported: model.cacheSupported ?? false
|
|
30
|
-
programmaticToolCallingSupported: model.programmaticToolCallingSupported ?? false
|
|
40
|
+
cacheSupported: (model.promptCache ?? provider.promptCache)?.mode !== undefined ? (model.promptCache ?? provider.promptCache)?.mode !== 'off' : model.cacheSupported ?? false
|
|
31
41
|
};
|
|
32
42
|
}
|
|
33
|
-
async *stream(request, config = this.config.current.config) {
|
|
43
|
+
async *stream(request, config = this.config.current.config, compiled = compileModelRequest(request, config)) {
|
|
44
|
+
assertContextBoundaries(request.messages);
|
|
34
45
|
const { providerId, modelId } = splitReference(request.model);
|
|
35
46
|
const providerConfig = config.providers[providerId];
|
|
36
47
|
const modelConfig = providerConfig?.models[modelId];
|
|
37
48
|
if (providerConfig === undefined || modelConfig === undefined)
|
|
38
49
|
fail('MODEL_NOT_FOUND', 404, `Model ${request.model} is not configured.`);
|
|
39
50
|
assertProviderManifest(providerManifest(config, providerId), request.model, this.target);
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
const requestId = randomUUID();
|
|
52
|
+
const report = this.audit === undefined ? undefined : (event) => {
|
|
53
|
+
this.audit?.append({ ...event, model_request_id: requestId, context: request.auditContext ?? null,
|
|
54
|
+
provider_id: providerId, model_id: modelConfig.wireModel ?? modelId,
|
|
55
|
+
input_manifest: request.messages.map(message => ({ role: message.role, context_item_id: message.contextItemId ?? null,
|
|
56
|
+
content_sha256: sha256(message.content), content_bytes: Buffer.byteLength(message.content),
|
|
57
|
+
tool_call_id: message.toolCallId ?? null, images: message.images ?? [] })) });
|
|
58
|
+
};
|
|
59
|
+
const resolved = createModel(providerId, modelConfig.wireModel ?? modelId, providerConfig, compiled.bodyOptions, report);
|
|
46
60
|
const ordinals = new Map();
|
|
47
|
-
const toolSet = tools(request.tools, programmatic, resolved.programmaticTool, callers, ordinals);
|
|
48
|
-
const instructions = request.messages.filter((message) => message.role === 'system').map((message) => message.content).join('\n\n');
|
|
49
|
-
const conversation = request.messages.filter((message) => message.role !== 'system');
|
|
50
61
|
const result = streamText({
|
|
51
62
|
model: resolved.model,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
tools:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
.filter((capabilityId) => capabilityId !== 'programmatic_tool_run' && capabilityId !== 'capability_search')
|
|
58
|
-
.map((capabilityId) => [capabilityId, ['programmatic_tool_run', 'AI_SDK_DIRECT_TOOL_CALL']])),
|
|
59
|
-
stopWhen: stepCountIs(programmatic.maxToolCalls + 1),
|
|
60
|
-
onToolExecutionStart: (event) => {
|
|
61
|
-
const caller = providerCaller(event.toolCall.providerMetadata);
|
|
62
|
-
if (caller.parentProviderCallId !== null)
|
|
63
|
-
callers.set(event.toolCall.toolCallId, caller.parentProviderCallId);
|
|
64
|
-
}
|
|
65
|
-
}),
|
|
63
|
+
allowSystemInMessages: true,
|
|
64
|
+
messages: await materializeImages(compiled.messages, request.messages, this.artifacts),
|
|
65
|
+
tools: tools(compiled.tools),
|
|
66
|
+
providerOptions: compiled.providerOptions,
|
|
67
|
+
toolChoice: compiled.toolChoice,
|
|
66
68
|
maxRetries: 0,
|
|
67
|
-
...(
|
|
68
|
-
abortSignal: request.signal
|
|
69
|
-
...(request.reasoningEffort === null ? {} : { providerOptions: reasoningOptions(providerId, providerConfig.adapter, request.reasoningEffort) })
|
|
69
|
+
...(compiled.maxOutputTokens === null ? {} : { maxOutputTokens: compiled.maxOutputTokens }),
|
|
70
|
+
abortSignal: request.signal
|
|
70
71
|
});
|
|
71
|
-
let
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
72
|
+
let streamCompleted = false;
|
|
73
|
+
try {
|
|
74
|
+
let ordinal = 0;
|
|
75
|
+
let finishReason;
|
|
76
|
+
const openBlocks = new Set();
|
|
77
|
+
let reportedCache;
|
|
78
|
+
for await (const part of result.stream) {
|
|
79
|
+
if (part.type === 'text-start')
|
|
80
|
+
openBlocks.add(`text:${part.id}`);
|
|
81
|
+
else if (part.type === 'text-end')
|
|
82
|
+
openBlocks.delete(`text:${part.id}`);
|
|
83
|
+
else if (part.type === 'reasoning-start')
|
|
84
|
+
openBlocks.add(`reasoning:${part.id}`);
|
|
85
|
+
else if (part.type === 'reasoning-end')
|
|
86
|
+
openBlocks.delete(`reasoning:${part.id}`);
|
|
87
|
+
else if (part.type === 'tool-input-start')
|
|
88
|
+
openBlocks.add(`tool:${part.id}`);
|
|
89
|
+
else if (part.type === 'tool-input-end')
|
|
90
|
+
openBlocks.delete(`tool:${part.id}`);
|
|
91
|
+
if (part.type === 'start-step' && part.warnings.length > 0)
|
|
92
|
+
yield { type: 'providerWarnings', warnings: jsonValue(part.warnings) };
|
|
93
|
+
else if (part.type === 'text-delta')
|
|
94
|
+
yield { type: 'text', text: part.text };
|
|
95
|
+
else if (part.type === 'reasoning-delta')
|
|
96
|
+
yield { type: 'thinking', text: part.text };
|
|
97
|
+
else if (part.type === 'tool-input-start') {
|
|
98
|
+
const toolOrdinal = ordinal++;
|
|
99
|
+
ordinals.set(part.id, toolOrdinal);
|
|
100
|
+
yield { type: 'toolStart', ordinal: toolOrdinal, id: part.id, name: part.toolName };
|
|
101
|
+
}
|
|
102
|
+
else if (part.type === 'tool-call') {
|
|
103
|
+
const toolOrdinal = ordinals.get(part.toolCallId) ?? ordinal++;
|
|
104
|
+
ordinals.set(part.toolCallId, toolOrdinal);
|
|
105
|
+
yield {
|
|
106
|
+
type: 'tool',
|
|
107
|
+
ordinal: toolOrdinal,
|
|
108
|
+
id: part.toolCallId,
|
|
109
|
+
name: part.toolName,
|
|
110
|
+
// Not `jsonValue`: a Provider hands back a raw JSON string when the model calls a tool that was
|
|
111
|
+
// not in the request's manifest, and storing that string poisons the Context permanently.
|
|
112
|
+
arguments: toolArguments(part.input),
|
|
113
|
+
...(part.providerExecuted === true ? { providerExecuted: true } : {}),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
else if (part.type === 'tool-result') {
|
|
117
|
+
const toolOrdinal = ordinals.get(part.toolCallId) ?? ordinal++;
|
|
118
|
+
ordinals.set(part.toolCallId, toolOrdinal);
|
|
119
|
+
yield {
|
|
120
|
+
type: 'toolResult',
|
|
121
|
+
ordinal: toolOrdinal,
|
|
122
|
+
id: part.toolCallId,
|
|
123
|
+
name: part.toolName,
|
|
124
|
+
output: jsonValue(part.output),
|
|
125
|
+
...(part.providerExecuted === true ? { providerExecuted: true } : {})
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
else if (part.type === 'finish-step' && providerConfig.adapter !== '@ai-sdk/openai-compatible') {
|
|
129
|
+
const raw = record(part.usage.raw);
|
|
130
|
+
const details = record(raw?.input_tokens_details);
|
|
131
|
+
const read = providerConfig.adapter === '@ai-sdk/openai' ? details?.cached_tokens : raw?.cache_read_input_tokens;
|
|
132
|
+
const write = providerConfig.adapter === '@ai-sdk/openai' ? details?.cache_write_tokens : raw?.cache_creation_input_tokens;
|
|
133
|
+
const noCache = providerConfig.adapter === '@ai-sdk/anthropic' ? raw?.input_tokens : typeof read === 'number' ? part.usage.inputTokenDetails.noCacheTokens : undefined;
|
|
134
|
+
reportedCache = { ...(typeof read === 'number' ? { cacheReadTokens: read } : {}), ...(typeof write === 'number' ? { cacheWriteTokens: write } : {}), ...(typeof noCache === 'number' ? { uncachedInputTokens: noCache } : {}) };
|
|
135
|
+
}
|
|
136
|
+
else if (part.type === 'finish') {
|
|
137
|
+
yield {
|
|
138
|
+
type: 'usage',
|
|
139
|
+
inputTokens: part.totalUsage.inputTokens ?? 0,
|
|
140
|
+
outputTokens: part.totalUsage.outputTokens ?? 0,
|
|
141
|
+
...(reportedCache ?? {
|
|
142
|
+
...(part.totalUsage.inputTokenDetails.cacheReadTokens === undefined ? {} : { cacheReadTokens: part.totalUsage.inputTokenDetails.cacheReadTokens }),
|
|
143
|
+
...(part.totalUsage.inputTokenDetails.cacheWriteTokens === undefined ? {} : { cacheWriteTokens: part.totalUsage.inputTokenDetails.cacheWriteTokens }),
|
|
144
|
+
...(part.totalUsage.inputTokenDetails.noCacheTokens === undefined ? {} : { uncachedInputTokens: part.totalUsage.inputTokenDetails.noCacheTokens })
|
|
145
|
+
})
|
|
146
|
+
};
|
|
147
|
+
finishReason = part.finishReason;
|
|
148
|
+
}
|
|
149
|
+
else if (part.type === 'error') {
|
|
150
|
+
throw part.error;
|
|
151
|
+
}
|
|
152
|
+
else if (part.type === 'abort') {
|
|
153
|
+
throw new Error(part.reason ?? 'Model request aborted.');
|
|
154
|
+
}
|
|
124
155
|
}
|
|
156
|
+
request.signal.throwIfAborted();
|
|
157
|
+
if (finishReason !== 'stop' && finishReason !== 'tool-calls')
|
|
158
|
+
fail('MODEL_RESPONSE_INCOMPLETE', 502, `Model response did not complete: ${finishReason ?? 'missing finish'}.`);
|
|
159
|
+
if (openBlocks.size > 0)
|
|
160
|
+
fail('MODEL_RESPONSE_INCOMPLETE', 502, 'Model response contains unfinished content blocks.');
|
|
161
|
+
const responses = await result.responseMessages;
|
|
162
|
+
const content = responses.flatMap(message => message.role === 'assistant' && Array.isArray(message.content) ? message.content : []);
|
|
163
|
+
// JSON serialization removes SDK optional undefined fields without interpreting opaque metadata.
|
|
164
|
+
const response = JSON.parse(JSON.stringify({ route: responseRoute(providerId, providerConfig, modelId), content }));
|
|
165
|
+
assertReasoningState(response, providerConfig.adapter, resolveProfile(config, modelConfig.harnessProfile));
|
|
166
|
+
report?.({ event_type: 'model_stream_completed', outcome: 'validated_response', finish_reason: finishReason });
|
|
167
|
+
streamCompleted = true;
|
|
168
|
+
yield { type: 'response', response };
|
|
169
|
+
yield { type: 'end', reason: finishReason };
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
if (!streamCompleted)
|
|
173
|
+
report?.({ event_type: 'model_stream_incomplete', outcome: 'unknown', cancelled: request.signal.aborted });
|
|
125
174
|
}
|
|
126
175
|
}
|
|
127
176
|
}
|
|
128
|
-
|
|
177
|
+
// This representation is shared by attempt diagnostics and the SDK send path.
|
|
178
|
+
// Local Context identities and annotations never enter the Provider fingerprint.
|
|
179
|
+
export function compileModelRequest(request, config, previous) {
|
|
180
|
+
assertContextBoundaries(request.messages);
|
|
181
|
+
const { providerId, modelId } = splitReference(request.model);
|
|
182
|
+
const provider = config.providers[providerId];
|
|
183
|
+
const model = provider?.models[modelId];
|
|
184
|
+
if (provider === undefined || model === undefined)
|
|
185
|
+
fail('MODEL_NOT_FOUND', 404, `Model ${request.model} is not configured.`);
|
|
186
|
+
const profile = resolveProfile(config, model.harnessProfile);
|
|
187
|
+
const reasoningEffort = request.reasoningEffort === null ? null : resolveReasoningEffort(config, request.model, request.reasoningEffort);
|
|
188
|
+
const bodyOptions = reasoningBody(provider.adapter, reasoningEffort);
|
|
189
|
+
if (profile.value.semantics.reasoningStateRoundTrip === 'required' && provider.adapter === '@ai-sdk/openai-compatible') {
|
|
190
|
+
fail('MODEL_REASONING_UNSUPPORTED', 400, 'This adapter cannot guarantee required reasoning state round trips.');
|
|
191
|
+
}
|
|
192
|
+
const route = responseRoute(providerId, provider, modelId);
|
|
193
|
+
for (const message of request.messages) {
|
|
194
|
+
if (message.response === undefined) {
|
|
195
|
+
if (profile.value.semantics.reasoningStateRoundTrip === 'required' && message.toolCalls?.length)
|
|
196
|
+
fail('MODEL_REASONING_STATE_MISSING', 409, 'Historical tool calls have no complete Provider response state.');
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (message.response.content.some(part => part.type === 'reasoning' || part.type === 'reasoning-file')) {
|
|
200
|
+
if (message.response.route !== route)
|
|
201
|
+
fail('MODEL_REASONING_STATE_INCOMPATIBLE', 409, 'Reasoning state cannot be replayed on a different Provider or model route.');
|
|
202
|
+
assertReasoningState(message.response, provider.adapter, profile);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const settings = model.promptCache ?? provider.promptCache;
|
|
206
|
+
const enabled = config.providerPromptCache?.enabled ?? profile.value.cache.enabledByDefault;
|
|
207
|
+
const mode = enabled ? settings?.mode ?? (model.cacheSupported === true ? 'implicit' : 'off') : 'off';
|
|
208
|
+
if ((mode === 'anthropic' && provider.adapter !== '@ai-sdk/anthropic') || (mode === 'openai-explicit' && provider.adapter !== '@ai-sdk/openai')) {
|
|
209
|
+
fail('CONFIG_INVALID', 400, `Cache mode ${mode} does not match ${provider.adapter}.`);
|
|
210
|
+
}
|
|
211
|
+
if ((mode === 'anthropic' && settings?.ttl === '30m') || (mode === 'openai-explicit' && settings?.ttl !== undefined && settings.ttl !== '30m')) {
|
|
212
|
+
fail('CONFIG_INVALID', 400, `Cache TTL does not match ${mode}.`);
|
|
213
|
+
}
|
|
214
|
+
const routeDigest = digest({ providerId, model: model.wireModel ?? modelId, adapter: provider.adapter, options: provider.options });
|
|
215
|
+
const rawTools = request.tools.map(({ id, description, inputSchema }) => ({ id, description, inputSchema })).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
216
|
+
const systems = request.messages.filter((message) => message.role === 'system');
|
|
217
|
+
const logical = [...systems, ...imageMessages(request.messages.filter((message) => message.role !== 'system'))];
|
|
218
|
+
const baselineSource = { systems: systems.map(({ role, content }) => ({ role, content })), tools: rawTools };
|
|
219
|
+
const sourceDigest = digest(baselineSource);
|
|
220
|
+
const prior = record(previous?.request);
|
|
221
|
+
const savedBaseline = record(prior?.baseline);
|
|
222
|
+
const reuseBaseline = prior?.routeDigest === routeDigest && savedBaseline?.sourceDigest === sourceDigest;
|
|
223
|
+
// Restore the actual finalized header, not a newly rendered variant, on restart/retry.
|
|
224
|
+
const baseline = reuseBaseline ? savedBaseline
|
|
225
|
+
: { sourceDigest, messages: JSON.parse(canonicalize(baselineSource.systems)), tools: JSON.parse(canonicalize(rawTools)) };
|
|
226
|
+
const definitions = [...baseline.tools];
|
|
227
|
+
const messages = JSON.parse(canonicalize([
|
|
228
|
+
...baseline.messages, ...logical.filter((message) => message.role !== 'system').map(message => aiMessage(message, route))
|
|
229
|
+
]));
|
|
230
|
+
if (mode === 'openai-explicit') {
|
|
231
|
+
for (const [index, message] of messages.entries()) {
|
|
232
|
+
if (message.role !== 'tool')
|
|
233
|
+
continue;
|
|
234
|
+
messages[index] = { ...message, content: message.content.map((part) => {
|
|
235
|
+
if (part.type !== 'tool-result')
|
|
236
|
+
return part;
|
|
237
|
+
const text = part.output.type === 'json' ? canonicalize(part.output.value) : part.output.type === 'text' ? part.output.value : '';
|
|
238
|
+
return { ...part, output: { type: 'content', value: [{ type: 'text', text }] } };
|
|
239
|
+
}) };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const namespace = provider.adapter === '@ai-sdk/openai' ? 'openai' : provider.adapter === '@ai-sdk/anthropic' ? 'anthropic' : providerId;
|
|
243
|
+
const providerOptions = {};
|
|
244
|
+
if (provider.adapter === '@ai-sdk/openai')
|
|
245
|
+
providerOptions.openai = { ...providerOptions.openai, store: false, include: ['reasoning.encrypted_content'] };
|
|
246
|
+
if (mode !== 'off' && provider.adapter === '@ai-sdk/openai') {
|
|
247
|
+
providerOptions[namespace] = {
|
|
248
|
+
...providerOptions[namespace],
|
|
249
|
+
...(request.cacheIdentity === undefined ? {} : { promptCacheKey: request.cacheIdentity }),
|
|
250
|
+
...(mode === 'openai-explicit' ? { promptCacheOptions: { mode: 'explicit', ttl: '30m' } } : {}),
|
|
251
|
+
...(settings?.retention === undefined ? {} : { promptCacheRetention: settings.retention })
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const minimum = settings?.minimumEligiblePrefixTokens ?? config.providerPromptCache?.minimumEligiblePrefixTokens ?? profile.value.cache.minimumEligiblePrefixTokens;
|
|
255
|
+
const maximum = Math.min(4, settings?.maximumBreakpoints ?? config.providerPromptCache?.maximumBreakpoints ?? profile.value.cache.maximumBreakpoints);
|
|
256
|
+
const toolsTokens = estimateTokens(canonicalize(definitions)).expected;
|
|
257
|
+
let cumulative = toolsTokens;
|
|
258
|
+
let blocks = definitions.length;
|
|
259
|
+
let prefix = digest({ routeDigest, tools: definitions, providerOptions, bodyOptions });
|
|
260
|
+
const entries = messages.map((message, ordinal) => {
|
|
261
|
+
cumulative += estimateTokens(canonicalize(message)).expected;
|
|
262
|
+
if (message.role === 'user' && Array.isArray(message.content))
|
|
263
|
+
cumulative += message.content.filter(part => part.type === 'image').length * imageTokenEstimate;
|
|
264
|
+
prefix = digest([prefix, digest(message)]);
|
|
265
|
+
blocks += typeof message.content === 'string' ? 1 : message.content.length;
|
|
266
|
+
return { ordinal, prefixTokens: cumulative, prefixDigest: prefix, eligible: cumulative >= minimum, blocks };
|
|
267
|
+
});
|
|
268
|
+
const { breakpoints, candidates, ttls } = selectCacheBreakpoints(logical, entries, {
|
|
269
|
+
mode, maximum, minimum, positions: settings?.positions ?? ['tool', 'system', 'message'], toolsTokens, hasTools: definitions.length > 0, ttl: settings?.ttl === '1h' ? '1h' : '5m',
|
|
270
|
+
supported: messages.map((message) => typeof message.content === 'string' ? message.content.length > 0
|
|
271
|
+
: mode === 'anthropic' ? message.content.length > 0 && !['reasoning', 'reasoning-file'].includes(message.content.at(-1)?.type ?? '') : message.role === 'tool'),
|
|
272
|
+
...(record(previous?.preparation)?.cache_plan === undefined ? {} : { previous: record(previous?.preparation)?.cache_plan })
|
|
273
|
+
});
|
|
274
|
+
for (const ordinal of breakpoints) {
|
|
275
|
+
const ttl = ttls[ordinal];
|
|
276
|
+
if (ordinal === -1) {
|
|
277
|
+
const index = definitions.length - 1;
|
|
278
|
+
const definition = definitions[index];
|
|
279
|
+
definitions[index] = { ...definition, providerOptions: { anthropic: { cacheControl: { type: 'ephemeral', ttl: ttl ?? '5m' } } } };
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const message = messages[ordinal];
|
|
283
|
+
const hint = mode === 'anthropic' ? { anthropic: { cacheControl: { type: 'ephemeral', ttl: ttl ?? settings?.ttl ?? '5m' } } } : { openai: { promptCacheBreakpoint: { mode: 'explicit' } } };
|
|
284
|
+
if (mode === 'openai-explicit' && message.role === 'tool') {
|
|
285
|
+
messages[ordinal] = { ...message, content: message.content.map((part, index) => {
|
|
286
|
+
if (index !== message.content.length - 1 || part.type !== 'tool-result' || part.output.type !== 'content')
|
|
287
|
+
return part;
|
|
288
|
+
const output = part.output;
|
|
289
|
+
return { ...part, output: { ...output, value: output.value.map((value, i) => i === output.value.length - 1 ? { ...value, providerOptions: hint } : value) } };
|
|
290
|
+
}) };
|
|
291
|
+
}
|
|
292
|
+
else if (message.role === 'system')
|
|
293
|
+
messages[ordinal] = { ...message, providerOptions: hint };
|
|
294
|
+
else if (typeof message.content === 'string')
|
|
295
|
+
messages[ordinal] = { ...message, content: [{ type: 'text', text: message.content, providerOptions: hint }] };
|
|
296
|
+
else
|
|
297
|
+
messages[ordinal] = { ...message, content: message.content.map((part, index) => {
|
|
298
|
+
if (index !== message.content.length - 1 || part.type === 'reasoning' || part.type === 'reasoning-file')
|
|
299
|
+
return part;
|
|
300
|
+
const options = 'providerOptions' in part ? part.providerOptions : undefined;
|
|
301
|
+
return { ...part, providerOptions: { ...options, [namespace]: { ...options?.[namespace], ...hint[namespace] } } };
|
|
302
|
+
}) };
|
|
303
|
+
}
|
|
304
|
+
// This Anthropic SDK removes schemas for `none`. Keep the cacheable schema prefix;
|
|
305
|
+
// the Compactor consumer rejects every tool event and never executes one.
|
|
306
|
+
const toolChoice = request.toolChoice === 'none' && provider.adapter !== '@ai-sdk/anthropic' ? 'none' : 'auto';
|
|
307
|
+
const fingerprint = digest({ model: model.wireModel ?? modelId, adapter: provider.adapter, messages: messages, tools: definitions, providerOptions, bodyOptions, toolChoice, maxOutputTokens: request.maxOutputTokens ?? null });
|
|
308
|
+
return {
|
|
309
|
+
version: 1, model: request.model, wireModel: model.wireModel ?? modelId, adapter: provider.adapter, routeDigest,
|
|
310
|
+
baseline, messages, tools: definitions, providerOptions, bodyOptions, toolChoice, reasoningEffort,
|
|
311
|
+
maxOutputTokens: request.maxOutputTokens ?? null,
|
|
312
|
+
cachePlan: { status: mode === 'off' ? 'disabled' : 'planned', mode, fingerprintVersion: 2, providerVisibleFingerprint: fingerprint,
|
|
313
|
+
hintsApplied: breakpoints.length > 0, breakpoints, candidates, ttls, entries, cacheIdentity: request.cacheIdentity ?? null,
|
|
314
|
+
model: request.model, routeDigest, systemDigest: digest(baseline.messages), toolsDigest: digest(baseline.tools), toolsTokens,
|
|
315
|
+
requestOptionsDigest: digest({ providerOptions, bodyOptions }), baselineRestored: reuseBaseline,
|
|
316
|
+
historyIds: logical.map((message) => message.contextItemId ?? ''), historyKinds: logical.map((message) => typeof message.metadata?.purpose === 'string' ? message.metadata.purpose : message.role),
|
|
317
|
+
snippedIds: logical.filter((message) => message.metadata?.snipped === true).map((message) => message.contextItemId ?? '') }
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function reasoningBody(adapter, effort) {
|
|
321
|
+
if (effort === null)
|
|
322
|
+
return {};
|
|
129
323
|
if (adapter === '@ai-sdk/openai')
|
|
130
|
-
return {
|
|
324
|
+
return { reasoning: { effort } };
|
|
131
325
|
if (adapter === '@ai-sdk/anthropic')
|
|
132
|
-
return {
|
|
133
|
-
return {
|
|
326
|
+
return { output_config: { effort } };
|
|
327
|
+
return { reasoning_effort: effort };
|
|
134
328
|
}
|
|
135
|
-
function createModel(providerId, modelId, config) {
|
|
329
|
+
function createModel(providerId, modelId, config, bodyOptions, report) {
|
|
136
330
|
const apiKey = resolveSecret(config.options.apiKey);
|
|
331
|
+
// Effort is user-owned. SDK enums must not reject or transform it before the
|
|
332
|
+
// endpoint sees it. Preserve other SDK-generated fields, headers and cancellation.
|
|
333
|
+
const transport = { fetch: (async (url, init) => {
|
|
334
|
+
const body = JSON.parse(init?.body);
|
|
335
|
+
for (const [key, value] of Object.entries(bodyOptions)) {
|
|
336
|
+
const fields = record(value);
|
|
337
|
+
body[key] = fields === undefined ? value : { ...record(body[key]), ...fields };
|
|
338
|
+
}
|
|
339
|
+
const serialized = JSON.stringify(body);
|
|
340
|
+
const destination = new URL(url instanceof Request ? url.url : url);
|
|
341
|
+
const transportId = randomUUID();
|
|
342
|
+
const base = { transport_id: transportId, destination_origin: destination.origin,
|
|
343
|
+
destination_path_sha256: sha256(destination.pathname), body_sha256: sha256(serialized), body_bytes: Buffer.byteLength(serialized) };
|
|
344
|
+
report?.({ ...base, event_type: 'model_send_started' });
|
|
345
|
+
let response;
|
|
346
|
+
try {
|
|
347
|
+
response = await fetch(url, { ...init, body: serialized, redirect: 'error' });
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
report?.({ ...base, event_type: 'model_send_failed', outcome: 'unknown' });
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
report?.({ ...base, event_type: 'model_response_headers', status: response.status,
|
|
354
|
+
outcome: 'headers_received_stream_not_confirmed' });
|
|
355
|
+
return response;
|
|
356
|
+
}) };
|
|
137
357
|
if (config.adapter === '@ai-sdk/openai') {
|
|
138
|
-
const provider = createOpenAI({ ...(apiKey === undefined ? {} : { apiKey }), ...(config.options.baseURL === undefined ? {} : { baseURL: config.options.baseURL }), ...(config.options.headers === undefined ? {} : { headers: config.options.headers }) });
|
|
139
|
-
return { model: provider(modelId)
|
|
358
|
+
const provider = createOpenAI({ ...transport, ...(apiKey === undefined ? {} : { apiKey }), ...(config.options.baseURL === undefined ? {} : { baseURL: config.options.baseURL }), ...(config.options.headers === undefined ? {} : { headers: config.options.headers }) });
|
|
359
|
+
return { model: provider(modelId) };
|
|
140
360
|
}
|
|
141
361
|
if (config.adapter === '@ai-sdk/anthropic') {
|
|
142
|
-
return { model: createAnthropic({ ...(apiKey === undefined ? {} : { apiKey }), ...(config.options.baseURL === undefined ? {} : { baseURL: config.options.baseURL }), ...(config.options.headers === undefined ? {} : { headers: config.options.headers }) })(modelId) };
|
|
362
|
+
return { model: createAnthropic({ ...transport, ...(apiKey === undefined ? {} : { apiKey }), ...(config.options.baseURL === undefined ? {} : { baseURL: config.options.baseURL }), ...(config.options.headers === undefined ? {} : { headers: config.options.headers }) })(modelId) };
|
|
143
363
|
}
|
|
144
364
|
if (config.options.baseURL === undefined)
|
|
145
365
|
fail('CONFIG_INVALID', 503, `Provider ${providerId} requires options.baseURL.`);
|
|
146
|
-
return { model: createOpenAICompatible({ name: providerId, baseURL: config.options.baseURL, ...(apiKey === undefined ? {} : { apiKey }), ...(config.options.headers === undefined ? {} : { headers: config.options.headers }) })(modelId) };
|
|
366
|
+
return { model: createOpenAICompatible({ ...transport, name: providerId, baseURL: config.options.baseURL, ...(apiKey === undefined ? {} : { apiKey }), ...(config.options.headers === undefined ? {} : { headers: config.options.headers }), convertUsage: compatibleUsage })(modelId) };
|
|
147
367
|
}
|
|
148
|
-
function
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
368
|
+
function compatibleUsage(usage) {
|
|
369
|
+
const number = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
370
|
+
const details = usage?.prompt_tokens_details;
|
|
371
|
+
const outputDetails = usage?.completion_tokens_details;
|
|
372
|
+
const total = number(usage?.prompt_tokens);
|
|
373
|
+
const read = number(usage?.prompt_cache_hit_tokens) ?? number(details?.cached_tokens);
|
|
374
|
+
const miss = number(usage?.prompt_cache_miss_tokens) ?? (total === undefined || read === undefined ? undefined : Math.max(0, total - read));
|
|
375
|
+
const output = number(usage?.completion_tokens);
|
|
376
|
+
const reasoning = number(outputDetails?.reasoning_tokens);
|
|
377
|
+
return { inputTokens: { total, noCache: miss, cacheRead: read, cacheWrite: undefined },
|
|
378
|
+
outputTokens: { total: output, text: output === undefined || reasoning === undefined ? undefined : Math.max(0, output - reasoning), reasoning } };
|
|
379
|
+
}
|
|
380
|
+
function tools(definitions) {
|
|
381
|
+
return Object.fromEntries(definitions.map((definition) => [definition.id, tool({
|
|
155
382
|
description: definition.description,
|
|
383
|
+
...(definition.providerOptions === undefined ? {} : { providerOptions: definition.providerOptions }),
|
|
156
384
|
inputSchema: jsonSchema(definition.inputSchema),
|
|
157
|
-
|
|
158
|
-
};
|
|
159
|
-
if (eligible) {
|
|
160
|
-
result[definition.id] = tool({
|
|
161
|
-
...descriptor,
|
|
162
|
-
providerOptions: { openai: { outputSchema: definition.outputSchema ?? { type: 'object', additionalProperties: true } } },
|
|
163
|
-
execute: (input, options) => {
|
|
164
|
-
const parentProviderCallId = callers.get(options.toolCallId);
|
|
165
|
-
return programmatic.execute({
|
|
166
|
-
capabilityId: definition.id,
|
|
167
|
-
arguments: jsonValue(input),
|
|
168
|
-
providerCallId: options.toolCallId,
|
|
169
|
-
ordinal: ordinals.get(options.toolCallId) ?? 0,
|
|
170
|
-
...(parentProviderCallId === undefined ? {} : { parentProviderCallId })
|
|
171
|
-
}, options.abortSignal);
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
176
|
-
result[definition.id] = tool(descriptor);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
if (programmatic !== undefined && providerTool !== undefined)
|
|
180
|
-
result.programmatic_tool_run = providerTool;
|
|
181
|
-
return result;
|
|
385
|
+
...(definition.outputSchema === undefined ? {} : { outputSchema: jsonSchema(definition.outputSchema) })
|
|
386
|
+
})]));
|
|
182
387
|
}
|
|
183
|
-
function
|
|
184
|
-
if (
|
|
185
|
-
return {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const id = caller.callerId ?? caller.caller_id ?? caller.toolId ?? caller.tool_id;
|
|
192
|
-
return { programmatic: true, parentProviderCallId: typeof id === 'string' && id.length > 0 ? id : null };
|
|
388
|
+
function aiMessage(message, route) {
|
|
389
|
+
if (message.role === 'assistant' && message.response?.route === route)
|
|
390
|
+
return { role: 'assistant', content: message.response.content };
|
|
391
|
+
if (message.role === 'user' && message.images?.length) {
|
|
392
|
+
return { role: 'user', content: [
|
|
393
|
+
{ type: 'text', text: message.content },
|
|
394
|
+
...message.images.map(image => ({ type: 'image', image: imageReference(image) }))
|
|
395
|
+
] };
|
|
193
396
|
}
|
|
194
|
-
return { programmatic: false, parentProviderCallId: null };
|
|
195
|
-
}
|
|
196
|
-
function isObject(value) {
|
|
197
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
198
|
-
}
|
|
199
|
-
function aiMessage(message) {
|
|
200
397
|
if (message.role === 'assistant' && message.toolCalls !== undefined) {
|
|
201
398
|
return {
|
|
202
399
|
role: 'assistant',
|
|
@@ -224,6 +421,34 @@ function aiMessage(message) {
|
|
|
224
421
|
}
|
|
225
422
|
return { role: message.role, content: message.content };
|
|
226
423
|
}
|
|
424
|
+
function responseRoute(providerId, provider, modelId) {
|
|
425
|
+
return digest({ providerId, adapter: provider.adapter, baseURL: provider.options.baseURL ?? null, model: provider.models[modelId]?.wireModel ?? modelId });
|
|
426
|
+
}
|
|
427
|
+
function assertReasoningState(response, adapter, profile) {
|
|
428
|
+
// OpenAI may split one reasoning item into multiple summaries; only the final
|
|
429
|
+
// summary carries its ciphertext, and the SDK rejoins them by itemId on replay.
|
|
430
|
+
const encryptedItems = new Set(response.content.flatMap(part => {
|
|
431
|
+
if (part.type !== 'reasoning')
|
|
432
|
+
return [];
|
|
433
|
+
const metadata = part.providerOptions?.openai;
|
|
434
|
+
return typeof metadata?.reasoningEncryptedContent === 'string' && typeof metadata.itemId === 'string' ? [metadata.itemId] : [];
|
|
435
|
+
}));
|
|
436
|
+
for (const part of response.content) {
|
|
437
|
+
if (part.type !== 'reasoning' && part.type !== 'reasoning-file')
|
|
438
|
+
continue;
|
|
439
|
+
if (profile.value.semantics.reasoningStateRoundTrip === 'forbidden' || profile.value.semantics.thinkingContinuation === 'forbidden') {
|
|
440
|
+
fail('MODEL_REASONING_STATE_FORBIDDEN', 409, 'The Model Profile forbids replaying reasoning state.');
|
|
441
|
+
}
|
|
442
|
+
const metadata = part.providerOptions;
|
|
443
|
+
if (adapter === '@ai-sdk/openai-compatible' && profile.value.semantics.reasoningStateRoundTrip === 'optional')
|
|
444
|
+
continue;
|
|
445
|
+
if (adapter === '@ai-sdk/anthropic' && (typeof metadata?.anthropic?.signature === 'string' || typeof metadata?.anthropic?.redactedData === 'string'))
|
|
446
|
+
continue;
|
|
447
|
+
if (adapter === '@ai-sdk/openai' && (typeof metadata?.openai?.reasoningEncryptedContent === 'string' || (typeof metadata?.openai?.itemId === 'string' && encryptedItems.has(metadata.openai.itemId))))
|
|
448
|
+
continue;
|
|
449
|
+
fail('MODEL_REASONING_STATE_MISSING', 502, 'The Provider returned reasoning without replayable state.');
|
|
450
|
+
}
|
|
451
|
+
}
|
|
227
452
|
function toolOutput(content) {
|
|
228
453
|
try {
|
|
229
454
|
const parsed = JSON.parse(content);
|
|
@@ -14,6 +14,7 @@ export declare class Observability {
|
|
|
14
14
|
constructor(root: string, config: HarnessConfig['observability']);
|
|
15
15
|
record(component: string, operation: string, outcome: string, durationMs: number, fields?: Readonly<Record<string, Json>>): void;
|
|
16
16
|
increment(name: string, amount?: number): void;
|
|
17
|
+
status(): Readonly<Record<string, Json>>;
|
|
17
18
|
logs(limit: number): readonly RuntimeRecord[];
|
|
18
19
|
traces(limit: number): readonly RuntimeRecord[];
|
|
19
20
|
agentOps(limit: number): readonly RuntimeRecord[];
|