narraeon 0.1.1 → 0.2.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/dist/node/protocol/modelConnections.js +49 -0
- package/dist/node/protocol/modelUsage.js +39 -0
- package/dist/node/protocol/v1.js +29 -0
- package/dist/node/runtime/V1Runtime.js +59 -23
- package/dist/node/runtime/model/FileNativeModelAdapters.js +691 -275
- package/dist/node/runtime/model/ModelConnectionStore.js +123 -2
- package/dist/node/runtime/model/ModelHost.js +74 -9
- package/dist/node/runtime/model/ModelHostStream.js +61 -1
- package/dist/node/runtime/model/SettingAuthorStream.js +16 -113
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +269 -0
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +1039 -0
- package/dist/node/runtime/play/PlayCallChain.js +609 -330
- package/dist/node/runtime/play/PlayDocumentTools.js +35 -0
- package/dist/node/runtime/play/PlayFollowupRequests.js +1 -4
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +45 -30
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +55 -29
- package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +9 -4
- package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +626 -0
- package/dist/node/runtime/world/FileNativeWorldStore.js +449 -191
- package/dist/web/assets/index-DHV_VWl3.js +1192 -0
- package/dist/web/assets/index-DjLCkP99.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-CRGTZ9Ak.css +0 -1
- package/dist/web/assets/index-DDx1itzT.js +0 -1192
|
@@ -2,18 +2,50 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { appendFile, chmod, mkdir, stat } from "node:fs/promises";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
4
|
import { defaultAppLocale, } from "../../protocol/appPreferences.js";
|
|
5
|
-
import {
|
|
5
|
+
import { hasCLIProxyThinkingSuffix, modelReasoningPolicyIssue, } from "../../protocol/modelConnections.js";
|
|
6
|
+
import { ModelHostBindingMismatchError, ModelHostContinuationError, ModelHostFailureError, ModelHostOutcomeUnknownError, } from "./ModelHost.js";
|
|
6
7
|
import { renderPromptDeltaMessage, } from "../prompt/FileNativePromptCompiler.js";
|
|
7
8
|
import { aggregateAnthropicSettingStream, aggregateChatSettingStream, } from "./SettingAuthorStream.js";
|
|
8
9
|
import { aggregateAnthropicModelStream, aggregateChatModelStream, aggregateResponsesModelStream, } from "./ModelHostStream.js";
|
|
9
|
-
import { defaultRuntimeToolDefinitionStrategy, } from "../prompt/FileNativeToolRegistry.js";
|
|
10
|
+
import { defaultRuntimeToolDefinitionStrategy, portableRuntimeToolInputSchema, } from "../prompt/FileNativeToolRegistry.js";
|
|
10
11
|
import { AiExchangeCapture, errorDescription, } from "./AiFailureLog.js";
|
|
12
|
+
const providerContinuationCodecVersion = 1;
|
|
13
|
+
function resolveConnection(connection) {
|
|
14
|
+
const resolved = structuredClone({
|
|
15
|
+
...connection,
|
|
16
|
+
dialect: connection.dialect ?? "standard",
|
|
17
|
+
reasoningEffort: connection.reasoningEffort ?? "provider_default",
|
|
18
|
+
reasoningSummary: connection.reasoningSummary ?? "provider_default",
|
|
19
|
+
thinkingMode: connection.thinkingMode ?? "provider_default",
|
|
20
|
+
thinkingBudgetTokens: connection.thinkingBudgetTokens ?? null,
|
|
21
|
+
});
|
|
22
|
+
const issue = modelReasoningPolicyIssue({
|
|
23
|
+
provider: resolved.provider,
|
|
24
|
+
dialect: resolved.dialect,
|
|
25
|
+
modelId: resolved.modelId,
|
|
26
|
+
effort: resolved.reasoningEffort,
|
|
27
|
+
summary: resolved.reasoningSummary,
|
|
28
|
+
thinking: resolved.thinkingMode,
|
|
29
|
+
thinkingBudgetTokens: resolved.thinkingBudgetTokens,
|
|
30
|
+
maxOutputTokens: resolved.maxOutputTokens,
|
|
31
|
+
});
|
|
32
|
+
if (issue !== null)
|
|
33
|
+
throw new Error(issue);
|
|
34
|
+
return resolved;
|
|
35
|
+
}
|
|
36
|
+
function modelCacheStrategy(connection) {
|
|
37
|
+
if (connection.provider === "anthropic_messages")
|
|
38
|
+
return "explicit_anthropic_blocks";
|
|
39
|
+
if (connection.dialect === "cliproxyapi")
|
|
40
|
+
return "explicit_cliproxyapi_message";
|
|
41
|
+
return "provider_managed";
|
|
42
|
+
}
|
|
11
43
|
export class FileNativeModelHost {
|
|
12
44
|
#connection;
|
|
13
45
|
#fetch;
|
|
14
46
|
#failureLog;
|
|
15
47
|
constructor(connection, fetchImplementation = fetch, failureLog) {
|
|
16
|
-
this.#connection =
|
|
48
|
+
this.#connection = resolveConnection(connection);
|
|
17
49
|
this.#fetch = fetchImplementation;
|
|
18
50
|
this.#failureLog = failureLog;
|
|
19
51
|
}
|
|
@@ -24,6 +56,7 @@ export class FileNativeModelHost {
|
|
|
24
56
|
modelId,
|
|
25
57
|
contextWindowTokens,
|
|
26
58
|
maxOutputTokens,
|
|
59
|
+
cacheStrategy: modelCacheStrategy(this.#connection),
|
|
27
60
|
endpointFingerprint: createHash("sha256")
|
|
28
61
|
.update(`${provider}\0${this.#connection.baseUrl}`)
|
|
29
62
|
.digest("hex"),
|
|
@@ -35,11 +68,19 @@ export class FileNativeModelHost {
|
|
|
35
68
|
responseStorage: provider === "openai_responses" ? "disabled" : "provider_default",
|
|
36
69
|
encryptedReasoningContinuation: provider === "openai_responses" ? "included" : "not_requested",
|
|
37
70
|
anthropicVersion: provider === "anthropic_messages" ? "2023-06-01" : null,
|
|
71
|
+
dialect: this.#connection.dialect,
|
|
72
|
+
reasoningEffort: this.#connection.reasoningEffort,
|
|
73
|
+
reasoningSummary: this.#connection.reasoningSummary,
|
|
74
|
+
thinkingMode: this.#connection.thinkingMode,
|
|
75
|
+
thinkingBudgetTokens: this.#connection.thinkingBudgetTokens,
|
|
76
|
+
cachePolicy: modelCacheStrategy(this.#connection),
|
|
77
|
+
providerContinuationCodecVersion,
|
|
38
78
|
}))
|
|
39
79
|
.digest("hex"),
|
|
40
80
|
};
|
|
41
81
|
}
|
|
42
82
|
async exchange(request, observer) {
|
|
83
|
+
assertExchangeCompatibility(this.#connection, request);
|
|
43
84
|
await trace("model_host_exchange", modelHostTrace(request));
|
|
44
85
|
if (this.#connection.provider === "chat_completions")
|
|
45
86
|
return this.#chat(request, observer?.onDelta);
|
|
@@ -47,23 +88,29 @@ export class FileNativeModelHost {
|
|
|
47
88
|
return this.#responses(request, observer?.onDelta);
|
|
48
89
|
return this.#anthropic(request, observer?.onDelta);
|
|
49
90
|
}
|
|
91
|
+
/** Uses the production encoders and deliberately omits credentials. */
|
|
92
|
+
previewRequest(request) {
|
|
93
|
+
assertExchangeCompatibility(this.#connection, request);
|
|
94
|
+
const provider = this.#connection.provider;
|
|
95
|
+
const body = provider === "chat_completions"
|
|
96
|
+
? chatRequestBody(this.#connection, request)
|
|
97
|
+
: provider === "openai_responses"
|
|
98
|
+
? responsesRequestBody(responsesRequestInput(this.#connection, request))
|
|
99
|
+
: anthropicRequestBody(this.#connection, request);
|
|
100
|
+
return {
|
|
101
|
+
provider,
|
|
102
|
+
method: "POST",
|
|
103
|
+
endpointPath: providerUrl(this.#connection).pathname,
|
|
104
|
+
headerNames: provider === "anthropic_messages"
|
|
105
|
+
? ["Accept", "anthropic-version", "x-api-key", "Content-Type"]
|
|
106
|
+
: ["Accept", "Authorization", "Content-Type"],
|
|
107
|
+
body: cloneProviderValue(body),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
50
110
|
async #responses(request, onDelta) {
|
|
51
111
|
return openAIResponsesRequest({
|
|
52
|
-
|
|
112
|
+
...responsesRequestInput(this.#connection, request),
|
|
53
113
|
fetchImplementation: this.#fetch,
|
|
54
|
-
input: [
|
|
55
|
-
...request.bootstrap.provider.messages.map(responsesBootstrapMessage),
|
|
56
|
-
...responsesAppend(request),
|
|
57
|
-
],
|
|
58
|
-
tools: providerToolDefinitions(request).map((tool) => ({
|
|
59
|
-
name: tool.name,
|
|
60
|
-
description: tool.description,
|
|
61
|
-
parameters: tool.inputSchema,
|
|
62
|
-
})),
|
|
63
|
-
allowedTools: providerAllowedTools(request),
|
|
64
|
-
toolStrategy: providerToolStrategy(request, this.#connection.provider),
|
|
65
|
-
hasFrozenToolPolicy: hasFrozenToolPolicy(request),
|
|
66
|
-
maxOutputTokens: request.maxOutputTokens,
|
|
67
114
|
tracePrefix: "responses",
|
|
68
115
|
...(this.#failureLog === undefined
|
|
69
116
|
? {}
|
|
@@ -73,37 +120,7 @@ export class FileNativeModelHost {
|
|
|
73
120
|
});
|
|
74
121
|
}
|
|
75
122
|
async #chat(request, onDelta) {
|
|
76
|
-
const
|
|
77
|
-
...request.bootstrap.provider.messages,
|
|
78
|
-
...chatAppend(request),
|
|
79
|
-
];
|
|
80
|
-
const tools = providerToolDefinitions(request);
|
|
81
|
-
const allowedTools = providerAllowedTools(request);
|
|
82
|
-
const hasPolicy = hasFrozenToolPolicy(request);
|
|
83
|
-
const body = {
|
|
84
|
-
model: this.#connection.modelId,
|
|
85
|
-
messages,
|
|
86
|
-
...(tools.length === 0
|
|
87
|
-
? {}
|
|
88
|
-
: {
|
|
89
|
-
tools: tools.map((tool) => ({
|
|
90
|
-
type: "function",
|
|
91
|
-
function: {
|
|
92
|
-
name: tool.name,
|
|
93
|
-
description: tool.description,
|
|
94
|
-
parameters: tool.inputSchema,
|
|
95
|
-
},
|
|
96
|
-
})),
|
|
97
|
-
}),
|
|
98
|
-
...(hasPolicy && allowedTools.length === 0
|
|
99
|
-
? { tool_choice: "none" }
|
|
100
|
-
: tools.length === 0
|
|
101
|
-
? {}
|
|
102
|
-
: { tool_choice: "auto" }),
|
|
103
|
-
max_tokens: request.maxOutputTokens,
|
|
104
|
-
stream: true,
|
|
105
|
-
stream_options: { include_usage: true },
|
|
106
|
-
};
|
|
123
|
+
const body = chatRequestBody(this.#connection, request);
|
|
107
124
|
const bodyJson = JSON.stringify(body);
|
|
108
125
|
await trace("chat_request", body);
|
|
109
126
|
const url = providerUrl(this.#connection);
|
|
@@ -135,6 +152,9 @@ export class FileNativeModelHost {
|
|
|
135
152
|
assistantMessage: cloneProviderValue(streamed.assistantMessage),
|
|
136
153
|
},
|
|
137
154
|
usage: normalizeChatUsage(streamed.usage),
|
|
155
|
+
...(streamed.stopReason === undefined
|
|
156
|
+
? {}
|
|
157
|
+
: { stopReason: streamed.stopReason }),
|
|
138
158
|
toolCalls: streamed.toolCalls.map((call) => ({
|
|
139
159
|
id: call.id,
|
|
140
160
|
name: call.name,
|
|
@@ -151,21 +171,14 @@ export class FileNativeModelHost {
|
|
|
151
171
|
const payload = rawPayload;
|
|
152
172
|
await trace("chat_response", payload);
|
|
153
173
|
const message = payload.choices?.[0]?.message;
|
|
174
|
+
const finishReason = payload.choices?.[0]?.finish_reason;
|
|
154
175
|
if (message === undefined)
|
|
155
176
|
throw unknownProviderResponse("Chat Completions");
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
typeof
|
|
159
|
-
(
|
|
160
|
-
|
|
161
|
-
typeof message.reasoning_content !== "string") ||
|
|
162
|
-
(message.tool_calls !== undefined &&
|
|
163
|
-
(!Array.isArray(message.tool_calls) ||
|
|
164
|
-
message.tool_calls.some((call) => !isRecord(call) ||
|
|
165
|
-
typeof call.id !== "string" ||
|
|
166
|
-
!isRecord(call.function) ||
|
|
167
|
-
typeof call.function.name !== "string" ||
|
|
168
|
-
typeof call.function.arguments !== "string"))))
|
|
177
|
+
if (finishReason !== undefined &&
|
|
178
|
+
finishReason !== null &&
|
|
179
|
+
typeof finishReason !== "string")
|
|
180
|
+
throw unknownProviderResponse("Chat Completions");
|
|
181
|
+
if (!validChatAssistantMessage(message))
|
|
169
182
|
throw unknownProviderResponse("Chat Completions");
|
|
170
183
|
capture?.setReasoning(message.reasoning_content ?? undefined);
|
|
171
184
|
return {
|
|
@@ -178,6 +191,9 @@ export class FileNativeModelHost {
|
|
|
178
191
|
assistantMessage: cloneProviderValue(message),
|
|
179
192
|
},
|
|
180
193
|
usage: normalizeChatUsage(payload.usage),
|
|
194
|
+
...(typeof finishReason === "string"
|
|
195
|
+
? { stopReason: finishReason }
|
|
196
|
+
: {}),
|
|
181
197
|
toolCalls: (message.tool_calls ?? []).map((call) => ({
|
|
182
198
|
id: call.id,
|
|
183
199
|
name: call.function.name,
|
|
@@ -187,26 +203,7 @@ export class FileNativeModelHost {
|
|
|
187
203
|
});
|
|
188
204
|
}
|
|
189
205
|
async #anthropic(request, onDelta) {
|
|
190
|
-
const
|
|
191
|
-
const body = {
|
|
192
|
-
model: this.#connection.modelId,
|
|
193
|
-
system: request.bootstrap.provider.system,
|
|
194
|
-
messages: [
|
|
195
|
-
...request.bootstrap.provider.messages.filter(({ role }) => role !== "system"),
|
|
196
|
-
...anthropicAppend(request),
|
|
197
|
-
],
|
|
198
|
-
...(tools.length === 0
|
|
199
|
-
? {}
|
|
200
|
-
: {
|
|
201
|
-
tools: tools.map((tool) => ({
|
|
202
|
-
name: tool.name,
|
|
203
|
-
description: tool.description,
|
|
204
|
-
input_schema: tool.inputSchema,
|
|
205
|
-
})),
|
|
206
|
-
}),
|
|
207
|
-
max_tokens: request.maxOutputTokens,
|
|
208
|
-
stream: true,
|
|
209
|
-
};
|
|
206
|
+
const body = anthropicRequestBody(this.#connection, request);
|
|
210
207
|
const bodyJson = JSON.stringify(body);
|
|
211
208
|
await trace("anthropic_request", body);
|
|
212
209
|
const url = providerUrl(this.#connection);
|
|
@@ -253,6 +250,9 @@ export class FileNativeModelHost {
|
|
|
253
250
|
: { stopReason: streamed.stopReason }),
|
|
254
251
|
},
|
|
255
252
|
usage: normalizeAnthropicUsage(streamed.usage),
|
|
253
|
+
...(typeof streamed.stopReason === "string"
|
|
254
|
+
? { stopReason: streamed.stopReason }
|
|
255
|
+
: {}),
|
|
256
256
|
};
|
|
257
257
|
}
|
|
258
258
|
const rawPayload = await providerJson(response, "Anthropic Messages");
|
|
@@ -260,14 +260,7 @@ export class FileNativeModelHost {
|
|
|
260
260
|
throw unknownProviderResponse("Anthropic Messages");
|
|
261
261
|
const payload = rawPayload;
|
|
262
262
|
const content = payload.content;
|
|
263
|
-
if (
|
|
264
|
-
typeof item.type !== "string" ||
|
|
265
|
-
(item.type === "text" && typeof item.text !== "string") ||
|
|
266
|
-
(item.type === "thinking" && typeof item.thinking !== "string") ||
|
|
267
|
-
(item.type === "tool_use" &&
|
|
268
|
-
(typeof item.id !== "string" ||
|
|
269
|
-
typeof item.name !== "string" ||
|
|
270
|
-
!("input" in item)))))
|
|
263
|
+
if (!validAnthropicContent(content))
|
|
271
264
|
throw unknownProviderResponse("Anthropic Messages");
|
|
272
265
|
await trace("anthropic_response", payload);
|
|
273
266
|
const reasoningContent = content
|
|
@@ -298,10 +291,21 @@ export class FileNativeModelHost {
|
|
|
298
291
|
: { stopReason: payload.stop_reason }),
|
|
299
292
|
},
|
|
300
293
|
usage: normalizeAnthropicUsage(payload.usage),
|
|
294
|
+
...(typeof payload.stop_reason === "string"
|
|
295
|
+
? { stopReason: payload.stop_reason }
|
|
296
|
+
: {}),
|
|
301
297
|
};
|
|
302
298
|
});
|
|
303
299
|
}
|
|
304
300
|
}
|
|
301
|
+
function assertExchangeCompatibility(connection, request) {
|
|
302
|
+
if (request.bootstrap.provider.protocol !== connection.provider)
|
|
303
|
+
throw new ModelHostBindingMismatchError("The compiled prompt protocol does not match the selected model host");
|
|
304
|
+
if (request.bootstrap.cache.strategy !== modelCacheStrategy(connection))
|
|
305
|
+
throw new ModelHostBindingMismatchError("The compiled prompt cache policy does not match the selected model host");
|
|
306
|
+
if (request.maxOutputTokens > connection.maxOutputTokens)
|
|
307
|
+
throw new ModelHostBindingMismatchError("The request output limit exceeds the selected model host binding");
|
|
308
|
+
}
|
|
305
309
|
function modelHostTrace(request) {
|
|
306
310
|
return {
|
|
307
311
|
requestId: request.requestId,
|
|
@@ -420,7 +424,7 @@ export class FileNativeSettingAuthorProvider {
|
|
|
420
424
|
#locale;
|
|
421
425
|
#exchange = 0;
|
|
422
426
|
constructor(connection, fetchImplementation = fetch, diagnostics) {
|
|
423
|
-
this.#connection =
|
|
427
|
+
this.#connection = resolveConnection(connection);
|
|
424
428
|
this.#fetch = fetchImplementation;
|
|
425
429
|
this.#failureLog = diagnostics?.failureLog;
|
|
426
430
|
this.#operationId = diagnostics?.operationId;
|
|
@@ -441,7 +445,13 @@ export class FileNativeSettingAuthorProvider {
|
|
|
441
445
|
if (this.#connection.provider === "chat_completions") {
|
|
442
446
|
const body = {
|
|
443
447
|
model: this.#connection.modelId,
|
|
444
|
-
|
|
448
|
+
...chatReasoningRequest(this.#connection),
|
|
449
|
+
messages: chatSettingMessages(this.#connection, request.messages),
|
|
450
|
+
...(this.#connection.dialect === "cliproxyapi"
|
|
451
|
+
? {
|
|
452
|
+
prompt_cache_key: settingPromptCacheKey(this.#connection, request.messages, this.#operationId),
|
|
453
|
+
}
|
|
454
|
+
: {}),
|
|
445
455
|
...(tools.length === 0
|
|
446
456
|
? {}
|
|
447
457
|
: {
|
|
@@ -485,6 +495,10 @@ export class FileNativeSettingAuthorProvider {
|
|
|
485
495
|
...(streamed.reasoningContent === ""
|
|
486
496
|
? {}
|
|
487
497
|
: { reasoningContent: streamed.reasoningContent }),
|
|
498
|
+
providerState: {
|
|
499
|
+
protocol: "chat_completions",
|
|
500
|
+
assistantMessage: cloneProviderValue(streamed.assistantMessage),
|
|
501
|
+
},
|
|
488
502
|
toolCalls: streamed.toolCalls.map((call) => ({
|
|
489
503
|
id: call.id,
|
|
490
504
|
name: call.name,
|
|
@@ -498,13 +512,14 @@ export class FileNativeSettingAuthorProvider {
|
|
|
498
512
|
const response = await openAIResponsesRequest({
|
|
499
513
|
connection: this.#connection,
|
|
500
514
|
fetchImplementation: this.#fetch,
|
|
501
|
-
input: request.messages
|
|
515
|
+
input: responsesSettingInput(this.#connection, request.messages),
|
|
502
516
|
tools: tools.map((tool) => ({
|
|
503
517
|
name: tool.name,
|
|
504
518
|
description: tool.description,
|
|
505
519
|
parameters: tool.schema,
|
|
506
520
|
})),
|
|
507
521
|
maxOutputTokens: Math.min(request.maxOutputTokens, this.#connection.maxOutputTokens),
|
|
522
|
+
promptCacheKey: settingPromptCacheKey(this.#connection, request.messages, this.#operationId),
|
|
508
523
|
tracePrefix: "setting_responses",
|
|
509
524
|
...(this.#failureLog === undefined
|
|
510
525
|
? {}
|
|
@@ -515,6 +530,9 @@ export class FileNativeSettingAuthorProvider {
|
|
|
515
530
|
return {
|
|
516
531
|
role: "assistant",
|
|
517
532
|
content: response.text ?? "",
|
|
533
|
+
...(response.reasoningContent === undefined
|
|
534
|
+
? {}
|
|
535
|
+
: { reasoningContent: response.reasoningContent }),
|
|
518
536
|
providerState: response.providerState,
|
|
519
537
|
toolCalls: (response.toolCalls ?? []).map((call) => ({
|
|
520
538
|
...call,
|
|
@@ -528,14 +546,18 @@ export class FileNativeSettingAuthorProvider {
|
|
|
528
546
|
}
|
|
529
547
|
const system = request.messages
|
|
530
548
|
.filter(({ role }) => role === "system")
|
|
531
|
-
.map(({ content }) =>
|
|
532
|
-
|
|
549
|
+
.map(({ content }, index, messages) => ({
|
|
550
|
+
type: "text",
|
|
551
|
+
text: content,
|
|
552
|
+
...(index === messages.length - 1
|
|
553
|
+
? { cache_control: { type: "ephemeral" } }
|
|
554
|
+
: {}),
|
|
555
|
+
}));
|
|
533
556
|
const body = {
|
|
534
557
|
model: this.#connection.modelId,
|
|
558
|
+
...anthropicReasoningRequest(this.#connection),
|
|
535
559
|
...(system.length === 0 ? {} : { system }),
|
|
536
|
-
messages: request.messages
|
|
537
|
-
.filter(({ role }) => role !== "system")
|
|
538
|
-
.map(anthropicSettingMessage),
|
|
560
|
+
messages: anthropicSettingMessages(request.messages),
|
|
539
561
|
...(tools.length === 0
|
|
540
562
|
? {}
|
|
541
563
|
: {
|
|
@@ -576,25 +598,41 @@ export class FileNativeSettingAuthorProvider {
|
|
|
576
598
|
...(streamed.reasoningContent === ""
|
|
577
599
|
? {}
|
|
578
600
|
: { reasoningContent: streamed.reasoningContent }),
|
|
601
|
+
providerState: {
|
|
602
|
+
protocol: "anthropic_messages",
|
|
603
|
+
content: cloneProviderValue(streamed.providerContent),
|
|
604
|
+
...(streamed.responseId === undefined
|
|
605
|
+
? {}
|
|
606
|
+
: { responseId: streamed.responseId }),
|
|
607
|
+
...(streamed.model === undefined ? {} : { model: streamed.model }),
|
|
608
|
+
...(streamed.stopReason === undefined
|
|
609
|
+
? {}
|
|
610
|
+
: { stopReason: streamed.stopReason }),
|
|
611
|
+
},
|
|
579
612
|
toolCalls: streamed.toolCalls.map((call) => ({
|
|
580
613
|
id: call.id,
|
|
581
614
|
name: call.name,
|
|
582
|
-
arguments: asArguments(
|
|
615
|
+
arguments: asArguments(structuredClone(call.arguments)),
|
|
583
616
|
})),
|
|
584
617
|
usage: settingAuthorUsage(normalizeAnthropicUsage(streamed.usage)),
|
|
585
618
|
};
|
|
586
619
|
});
|
|
587
620
|
}
|
|
588
621
|
}
|
|
589
|
-
//
|
|
590
|
-
//
|
|
622
|
+
// Setting authoring uses the same complete usage projection as play. Missing
|
|
623
|
+
// Provider usage stays unavailable instead of being counted as zero.
|
|
591
624
|
function settingAuthorUsage(usage) {
|
|
592
|
-
if (usage
|
|
593
|
-
return
|
|
594
|
-
return {
|
|
595
|
-
inputTokens:
|
|
596
|
-
|
|
597
|
-
|
|
625
|
+
if (usage !== undefined)
|
|
626
|
+
return structuredClone(usage);
|
|
627
|
+
return usageRecord({
|
|
628
|
+
inputTokens: null,
|
|
629
|
+
uncachedInputTokens: null,
|
|
630
|
+
cacheReadTokens: null,
|
|
631
|
+
cacheWriteTokens: null,
|
|
632
|
+
reasoningTokens: null,
|
|
633
|
+
outputTokens: null,
|
|
634
|
+
totalTokens: null,
|
|
635
|
+
});
|
|
598
636
|
}
|
|
599
637
|
function settingToolDefinition(name, locale = defaultAppLocale) {
|
|
600
638
|
const descriptions = settingToolDescriptions[locale];
|
|
@@ -698,6 +736,126 @@ const settingToolDescriptions = {
|
|
|
698
736
|
function items(request) {
|
|
699
737
|
return request.appended;
|
|
700
738
|
}
|
|
739
|
+
function chatRequestBody(connection, request) {
|
|
740
|
+
const tools = providerToolDefinitions(request);
|
|
741
|
+
const allowedTools = providerAllowedTools(request);
|
|
742
|
+
const hasPolicy = hasFrozenToolPolicy(request);
|
|
743
|
+
return {
|
|
744
|
+
model: connection.modelId,
|
|
745
|
+
...chatReasoningRequest(connection),
|
|
746
|
+
messages: [
|
|
747
|
+
...chatBootstrapMessages(connection, request),
|
|
748
|
+
...chatAppend(request),
|
|
749
|
+
],
|
|
750
|
+
...(connection.dialect === "cliproxyapi"
|
|
751
|
+
? { prompt_cache_key: promptCacheKey(connection, request) }
|
|
752
|
+
: {}),
|
|
753
|
+
...(tools.length === 0
|
|
754
|
+
? {}
|
|
755
|
+
: {
|
|
756
|
+
tools: tools.map((tool) => ({
|
|
757
|
+
type: "function",
|
|
758
|
+
function: {
|
|
759
|
+
name: tool.name,
|
|
760
|
+
description: tool.description,
|
|
761
|
+
parameters: tool.inputSchema,
|
|
762
|
+
},
|
|
763
|
+
})),
|
|
764
|
+
}),
|
|
765
|
+
...(hasPolicy && allowedTools.length === 0
|
|
766
|
+
? { tool_choice: "none" }
|
|
767
|
+
: tools.length === 0
|
|
768
|
+
? {}
|
|
769
|
+
: { tool_choice: "auto" }),
|
|
770
|
+
max_tokens: request.maxOutputTokens,
|
|
771
|
+
stream: true,
|
|
772
|
+
stream_options: { include_usage: true },
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
function anthropicRequestBody(connection, request) {
|
|
776
|
+
const tools = providerToolDefinitions(request);
|
|
777
|
+
return {
|
|
778
|
+
model: connection.modelId,
|
|
779
|
+
...anthropicReasoningRequest(connection),
|
|
780
|
+
system: request.bootstrap.provider.system,
|
|
781
|
+
messages: [
|
|
782
|
+
...request.bootstrap.provider.messages.filter(({ role }) => role !== "system"),
|
|
783
|
+
...anthropicAppend(request),
|
|
784
|
+
],
|
|
785
|
+
...(tools.length === 0
|
|
786
|
+
? {}
|
|
787
|
+
: {
|
|
788
|
+
tools: tools.map((tool) => ({
|
|
789
|
+
name: tool.name,
|
|
790
|
+
description: tool.description,
|
|
791
|
+
input_schema: tool.inputSchema,
|
|
792
|
+
})),
|
|
793
|
+
}),
|
|
794
|
+
...(hasFrozenToolPolicy(request) &&
|
|
795
|
+
providerAllowedTools(request).length === 0
|
|
796
|
+
? { tool_choice: { type: "none" } }
|
|
797
|
+
: {}),
|
|
798
|
+
max_tokens: request.maxOutputTokens,
|
|
799
|
+
stream: true,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
function responsesRequestInput(connection, request) {
|
|
803
|
+
return {
|
|
804
|
+
connection,
|
|
805
|
+
input: [
|
|
806
|
+
...responsesBootstrapMessages(connection, request),
|
|
807
|
+
...responsesAppend(request),
|
|
808
|
+
],
|
|
809
|
+
tools: providerToolDefinitions(request).map((tool) => ({
|
|
810
|
+
name: tool.name,
|
|
811
|
+
description: tool.description,
|
|
812
|
+
parameters: tool.inputSchema,
|
|
813
|
+
})),
|
|
814
|
+
allowedTools: providerAllowedTools(request),
|
|
815
|
+
toolStrategy: providerToolStrategy(request, connection.provider),
|
|
816
|
+
hasFrozenToolPolicy: hasFrozenToolPolicy(request),
|
|
817
|
+
maxOutputTokens: request.maxOutputTokens,
|
|
818
|
+
promptCacheKey: promptCacheKey(connection, request),
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
function responsesRequestBody(input) {
|
|
822
|
+
const allowedTools = input.allowedTools ?? input.tools.map((tool) => tool.name);
|
|
823
|
+
const toolChoice = input.hasFrozenToolPolicy === true && allowedTools.length === 0
|
|
824
|
+
? "none"
|
|
825
|
+
: input.hasFrozenToolPolicy === true &&
|
|
826
|
+
input.toolStrategy === "native_allowed_subset"
|
|
827
|
+
? {
|
|
828
|
+
type: "allowed_tools",
|
|
829
|
+
mode: "auto",
|
|
830
|
+
tools: allowedTools.map((name) => ({ type: "function", name })),
|
|
831
|
+
}
|
|
832
|
+
: "auto";
|
|
833
|
+
return {
|
|
834
|
+
model: input.connection.modelId,
|
|
835
|
+
...responsesReasoningRequest(resolveConnection(input.connection)),
|
|
836
|
+
input: input.input,
|
|
837
|
+
...(input.tools.length === 0
|
|
838
|
+
? input.hasFrozenToolPolicy === true && allowedTools.length === 0
|
|
839
|
+
? { tool_choice: "none" }
|
|
840
|
+
: {}
|
|
841
|
+
: {
|
|
842
|
+
tools: input.tools.map((tool) => ({
|
|
843
|
+
type: "function",
|
|
844
|
+
name: tool.name,
|
|
845
|
+
description: tool.description,
|
|
846
|
+
parameters: tool.parameters,
|
|
847
|
+
})),
|
|
848
|
+
tool_choice: toolChoice,
|
|
849
|
+
}),
|
|
850
|
+
max_output_tokens: input.maxOutputTokens,
|
|
851
|
+
...(input.promptCacheKey === undefined
|
|
852
|
+
? {}
|
|
853
|
+
: { prompt_cache_key: input.promptCacheKey }),
|
|
854
|
+
store: false,
|
|
855
|
+
include: ["reasoning.encrypted_content"],
|
|
856
|
+
stream: true,
|
|
857
|
+
};
|
|
858
|
+
}
|
|
701
859
|
/**
|
|
702
860
|
* A frozen policy is present when the caller supplies the logical operation's
|
|
703
861
|
* complete tool universe. All play lifecycles share this one ModelHost seam.
|
|
@@ -712,7 +870,26 @@ function frozenToolPolicy(request) {
|
|
|
712
870
|
}
|
|
713
871
|
function providerToolDefinitions(request) {
|
|
714
872
|
const policy = frozenToolPolicy(request);
|
|
715
|
-
return policy?.toolUniverse ?? request.tools
|
|
873
|
+
return (policy?.toolUniverse ?? request.tools).map((tool) => {
|
|
874
|
+
if (!hasUnsupportedProviderToolSchemaRoot(tool.inputSchema))
|
|
875
|
+
return tool;
|
|
876
|
+
const portableInputSchema = portableRuntimeToolInputSchema(tool.name);
|
|
877
|
+
if (portableInputSchema === null)
|
|
878
|
+
throw new Error(`Provider tool ${tool.name} uses oneOf, allOf, or anyOf at the input schema root`);
|
|
879
|
+
if (hasUnsupportedProviderToolSchemaRoot(portableInputSchema))
|
|
880
|
+
throw new Error(`Provider tool ${tool.name} has no portable root-object input schema`);
|
|
881
|
+
// Released play contexts freeze their logical tool universe. Migrate only
|
|
882
|
+
// the Provider wire projection of the old root-union schema; Runtime keeps
|
|
883
|
+
// validating the original source-specific contract and the persisted
|
|
884
|
+
// transcript remains untouched.
|
|
885
|
+
return {
|
|
886
|
+
...tool,
|
|
887
|
+
inputSchema: portableInputSchema,
|
|
888
|
+
};
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
function hasUnsupportedProviderToolSchemaRoot(schema) {
|
|
892
|
+
return ["oneOf", "allOf", "anyOf"].some((keyword) => Object.prototype.hasOwnProperty.call(schema, keyword));
|
|
716
893
|
}
|
|
717
894
|
function providerAllowedTools(request) {
|
|
718
895
|
const policy = frozenToolPolicy(request);
|
|
@@ -726,82 +903,127 @@ function providerToolStrategy(request, provider) {
|
|
|
726
903
|
return policy.toolStrategy;
|
|
727
904
|
return defaultRuntimeToolDefinitionStrategy(provider);
|
|
728
905
|
}
|
|
906
|
+
function chatBootstrapMessages(connection, request) {
|
|
907
|
+
const messages = request.bootstrap.provider.messages.map((message) => cloneProviderValue(message));
|
|
908
|
+
return connection.dialect === "cliproxyapi"
|
|
909
|
+
? attachCacheControlToLastRole(messages, "system")
|
|
910
|
+
: messages;
|
|
911
|
+
}
|
|
912
|
+
function responsesBootstrapMessages(connection, request) {
|
|
913
|
+
const messages = request.bootstrap.provider.messages.map((message) => responsesBootstrapMessage(message, connection.dialect === "cliproxyapi"));
|
|
914
|
+
return connection.dialect === "cliproxyapi"
|
|
915
|
+
? attachCacheControlToLastRole(messages, "system")
|
|
916
|
+
: messages;
|
|
917
|
+
}
|
|
918
|
+
function attachCacheControlToLastRole(messages, role) {
|
|
919
|
+
const result = cloneProviderValue(messages);
|
|
920
|
+
let target = -1;
|
|
921
|
+
for (const [index, message] of result.entries())
|
|
922
|
+
if (isRecord(message) && message.role === role)
|
|
923
|
+
target = index;
|
|
924
|
+
if (target < 0)
|
|
925
|
+
return result;
|
|
926
|
+
const message = result[target];
|
|
927
|
+
if (!isRecord(message))
|
|
928
|
+
return result;
|
|
929
|
+
result[target] = {
|
|
930
|
+
...message,
|
|
931
|
+
cache_control: { type: "ephemeral" },
|
|
932
|
+
};
|
|
933
|
+
return result;
|
|
934
|
+
}
|
|
935
|
+
function promptCacheKey(connection, request) {
|
|
936
|
+
const stablePrefixScope = request.bootstrap.cache.stablePrefixFingerprint ??
|
|
937
|
+
createHash("sha256")
|
|
938
|
+
.update(JSON.stringify({
|
|
939
|
+
provider: request.bootstrap.provider,
|
|
940
|
+
tools: request.toolUniverse ?? request.tools,
|
|
941
|
+
toolStrategy: request.toolStrategy,
|
|
942
|
+
}))
|
|
943
|
+
.digest("hex");
|
|
944
|
+
// OpenAI uses this as a cache-routing hint, so equal frozen prefixes should
|
|
945
|
+
// share it. CLIProxyAPI additionally treats it as execution-session identity;
|
|
946
|
+
// there it must stay stable within one chain and isolated across chains.
|
|
947
|
+
const scope = connection.dialect === "cliproxyapi"
|
|
948
|
+
? (request.operationId ?? stablePrefixScope)
|
|
949
|
+
: stablePrefixScope;
|
|
950
|
+
return createHash("sha256")
|
|
951
|
+
.update(`${connection.provider}\0${connection.dialect}\0${connection.modelId}\0${scope}`)
|
|
952
|
+
.digest("hex");
|
|
953
|
+
}
|
|
954
|
+
function settingPromptCacheKey(connection, messages, operationId) {
|
|
955
|
+
const firstAssistant = messages.findIndex(({ role }) => role === "assistant");
|
|
956
|
+
const initialPrefix = messages.slice(0, firstAssistant < 0 ? messages.length : firstAssistant);
|
|
957
|
+
const stablePrefixScope = JSON.stringify(initialPrefix);
|
|
958
|
+
const scope = connection.dialect === "cliproxyapi"
|
|
959
|
+
? (operationId ?? stablePrefixScope)
|
|
960
|
+
: stablePrefixScope;
|
|
961
|
+
return createHash("sha256")
|
|
962
|
+
.update(`${connection.provider}\0${connection.dialect}\0${connection.modelId}\0${scope}`)
|
|
963
|
+
.digest("hex");
|
|
964
|
+
}
|
|
729
965
|
function chatAppend(request) {
|
|
730
966
|
return items(request).flatMap((item) => {
|
|
731
967
|
if (item.kind === "player")
|
|
732
|
-
return { role: "user", content: item.text };
|
|
968
|
+
return [{ role: "user", content: item.text }];
|
|
733
969
|
if (item.kind === "prompt_delta")
|
|
734
970
|
return promptDeltaProviderMessages(item.logicalMessages);
|
|
735
971
|
if (item.kind === "tool")
|
|
736
|
-
return
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
972
|
+
return [
|
|
973
|
+
{
|
|
974
|
+
role: "tool",
|
|
975
|
+
tool_call_id: item.toolCallId,
|
|
976
|
+
content: item.markdown,
|
|
977
|
+
},
|
|
978
|
+
];
|
|
741
979
|
if (item.providerState?.protocol === "chat_completions") {
|
|
742
980
|
const message = item.providerState.assistantMessage;
|
|
743
|
-
if (
|
|
981
|
+
if (validChatAssistantMessage(message))
|
|
744
982
|
return [cloneProviderValue(message)];
|
|
745
983
|
}
|
|
746
|
-
|
|
747
|
-
role: "assistant",
|
|
748
|
-
content: item.text,
|
|
749
|
-
...(item.reasoningContent === undefined
|
|
750
|
-
? {}
|
|
751
|
-
: { reasoning_content: item.reasoningContent }),
|
|
752
|
-
...(item.toolCalls.length === 0
|
|
753
|
-
? {}
|
|
754
|
-
: {
|
|
755
|
-
tool_calls: item.toolCalls.map((call) => ({
|
|
756
|
-
id: call.id,
|
|
757
|
-
type: "function",
|
|
758
|
-
function: {
|
|
759
|
-
name: call.name,
|
|
760
|
-
arguments: JSON.stringify(call.arguments),
|
|
761
|
-
},
|
|
762
|
-
})),
|
|
763
|
-
}),
|
|
764
|
-
};
|
|
765
|
-
return assistant;
|
|
984
|
+
throw continuationError("Chat Completions");
|
|
766
985
|
});
|
|
767
986
|
}
|
|
768
987
|
function anthropicAppend(request) {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
if (
|
|
773
|
-
return
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
};
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
}
|
|
804
|
-
|
|
988
|
+
const messages = [];
|
|
989
|
+
let toolResults = [];
|
|
990
|
+
const flushToolResults = () => {
|
|
991
|
+
if (toolResults.length === 0)
|
|
992
|
+
return;
|
|
993
|
+
messages.push({ role: "user", content: toolResults });
|
|
994
|
+
toolResults = [];
|
|
995
|
+
};
|
|
996
|
+
for (const item of items(request)) {
|
|
997
|
+
if (item.kind === "tool") {
|
|
998
|
+
toolResults.push({
|
|
999
|
+
type: "tool_result",
|
|
1000
|
+
tool_use_id: item.toolCallId,
|
|
1001
|
+
content: item.markdown,
|
|
1002
|
+
...(item.isError === true ? { is_error: true } : {}),
|
|
1003
|
+
});
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
flushToolResults();
|
|
1007
|
+
if (item.kind === "player") {
|
|
1008
|
+
messages.push({ role: "user", content: item.text });
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
if (item.kind === "prompt_delta") {
|
|
1012
|
+
messages.push(...promptDeltaProviderMessages(item.logicalMessages));
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
if (item.providerState?.protocol === "anthropic_messages" &&
|
|
1016
|
+
validAnthropicContent(item.providerState.content)) {
|
|
1017
|
+
messages.push({
|
|
1018
|
+
role: "assistant",
|
|
1019
|
+
content: cloneProviderValue(item.providerState.content),
|
|
1020
|
+
});
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
throw continuationError("Anthropic Messages");
|
|
1024
|
+
}
|
|
1025
|
+
flushToolResults();
|
|
1026
|
+
return messages;
|
|
805
1027
|
}
|
|
806
1028
|
function responsesAppend(request) {
|
|
807
1029
|
return items(request).flatMap((item) => {
|
|
@@ -817,9 +1039,10 @@ function responsesAppend(request) {
|
|
|
817
1039
|
output: item.markdown,
|
|
818
1040
|
},
|
|
819
1041
|
];
|
|
820
|
-
if (item.providerState?.protocol === "openai_responses"
|
|
1042
|
+
if (item.providerState?.protocol === "openai_responses" &&
|
|
1043
|
+
validResponsesOutput(item.providerState.output))
|
|
821
1044
|
return cloneResponseOutput(item.providerState.output);
|
|
822
|
-
|
|
1045
|
+
throw continuationError("OpenAI Responses");
|
|
823
1046
|
});
|
|
824
1047
|
}
|
|
825
1048
|
function promptDeltaProviderMessages(messages) {
|
|
@@ -828,6 +1051,77 @@ function promptDeltaProviderMessages(messages) {
|
|
|
828
1051
|
content: renderPromptDeltaMessage(message.role, message.markdown),
|
|
829
1052
|
}));
|
|
830
1053
|
}
|
|
1054
|
+
function chatReasoningRequest(connection) {
|
|
1055
|
+
return {
|
|
1056
|
+
...(connection.reasoningEffort === "provider_default"
|
|
1057
|
+
? {}
|
|
1058
|
+
: { reasoning_effort: connection.reasoningEffort }),
|
|
1059
|
+
...(connection.dialect !== "cliproxyapi" ||
|
|
1060
|
+
connection.reasoningSummary === "provider_default"
|
|
1061
|
+
? {}
|
|
1062
|
+
: {
|
|
1063
|
+
// Chat Completions has no standard summary switch. CLIProxyAPI
|
|
1064
|
+
// deliberately accepts the OpenRouter-compatible visibility bit and
|
|
1065
|
+
// maps it to the target protocol without changing effort.
|
|
1066
|
+
reasoning: {
|
|
1067
|
+
exclude: connection.reasoningSummary === "none",
|
|
1068
|
+
},
|
|
1069
|
+
}),
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
function responsesReasoningRequest(connection) {
|
|
1073
|
+
const reasoning = {
|
|
1074
|
+
...(connection.reasoningEffort === "provider_default"
|
|
1075
|
+
? {}
|
|
1076
|
+
: { effort: connection.reasoningEffort }),
|
|
1077
|
+
...(connection.reasoningSummary === "provider_default" ||
|
|
1078
|
+
(connection.reasoningSummary === "none" &&
|
|
1079
|
+
connection.dialect === "standard")
|
|
1080
|
+
? {}
|
|
1081
|
+
: {
|
|
1082
|
+
summary: connection.reasoningSummary === "none"
|
|
1083
|
+
? null
|
|
1084
|
+
: connection.reasoningSummary,
|
|
1085
|
+
}),
|
|
1086
|
+
};
|
|
1087
|
+
return Object.keys(reasoning).length === 0 ? {} : { reasoning };
|
|
1088
|
+
}
|
|
1089
|
+
function anthropicReasoningRequest(connection) {
|
|
1090
|
+
const display = connection.reasoningSummary === "provider_default"
|
|
1091
|
+
? {}
|
|
1092
|
+
: {
|
|
1093
|
+
display: connection.reasoningSummary === "auto" ? "summarized" : "omitted",
|
|
1094
|
+
};
|
|
1095
|
+
const thinking = connection.thinkingMode === "provider_default"
|
|
1096
|
+
? connection.dialect === "cliproxyapi" &&
|
|
1097
|
+
hasCLIProxyThinkingSuffix(connection.modelId) &&
|
|
1098
|
+
connection.reasoningSummary !== "provider_default"
|
|
1099
|
+
? {
|
|
1100
|
+
// CLIProxyAPI extracts visibility from a valid source thinking
|
|
1101
|
+
// block before the model suffix replaces its mode. The suffix is
|
|
1102
|
+
// still the sole mode/effort authority; adaptive is only the
|
|
1103
|
+
// proxy's protocol-valid summary carrier.
|
|
1104
|
+
thinking: { type: "adaptive", ...display },
|
|
1105
|
+
}
|
|
1106
|
+
: {}
|
|
1107
|
+
: connection.thinkingMode === "disabled"
|
|
1108
|
+
? { thinking: { type: "disabled" } }
|
|
1109
|
+
: connection.thinkingMode === "adaptive"
|
|
1110
|
+
? { thinking: { type: "adaptive", ...display } }
|
|
1111
|
+
: {
|
|
1112
|
+
thinking: {
|
|
1113
|
+
type: "enabled",
|
|
1114
|
+
budget_tokens: connection.thinkingBudgetTokens,
|
|
1115
|
+
...display,
|
|
1116
|
+
},
|
|
1117
|
+
};
|
|
1118
|
+
return {
|
|
1119
|
+
...thinking,
|
|
1120
|
+
...(connection.reasoningEffort === "provider_default"
|
|
1121
|
+
? {}
|
|
1122
|
+
: { output_config: { effort: connection.reasoningEffort } }),
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
831
1125
|
function providerUrl(connection) {
|
|
832
1126
|
const url = new URL(connection.baseUrl);
|
|
833
1127
|
const suffix = connection.provider === "chat_completions"
|
|
@@ -863,6 +1157,12 @@ async function providerError(response) {
|
|
|
863
1157
|
});
|
|
864
1158
|
return new ModelHostFailureError(`Provider request failed: ${response.status} ${text}`);
|
|
865
1159
|
}
|
|
1160
|
+
function chatSettingMessages(connection, messages) {
|
|
1161
|
+
const translated = messages.map(chatSettingMessage);
|
|
1162
|
+
if (connection.dialect !== "cliproxyapi")
|
|
1163
|
+
return translated;
|
|
1164
|
+
return attachCacheControlAtIndex(attachCacheControlToLastRole(translated, "system"), settingPrefixUserIndex(messages));
|
|
1165
|
+
}
|
|
866
1166
|
function chatSettingMessage(message) {
|
|
867
1167
|
if (message.role === "tool")
|
|
868
1168
|
return {
|
|
@@ -870,58 +1170,91 @@ function chatSettingMessage(message) {
|
|
|
870
1170
|
tool_call_id: message.toolCallId,
|
|
871
1171
|
content: message.content,
|
|
872
1172
|
};
|
|
1173
|
+
if (message.role === "assistant") {
|
|
1174
|
+
const state = message.providerState;
|
|
1175
|
+
if (state?.protocol === "chat_completions" &&
|
|
1176
|
+
validChatAssistantMessage(state.assistantMessage))
|
|
1177
|
+
return cloneProviderValue(state.assistantMessage);
|
|
1178
|
+
throw continuationError("Chat Completions setting improvement");
|
|
1179
|
+
}
|
|
873
1180
|
return {
|
|
874
|
-
role: message.role === "
|
|
875
|
-
? "assistant"
|
|
876
|
-
: message.role === "system"
|
|
877
|
-
? "system"
|
|
878
|
-
: "user",
|
|
1181
|
+
role: message.role === "system" ? "system" : "user",
|
|
879
1182
|
content: message.content,
|
|
880
|
-
...(message.role !== "assistant" || message.reasoningContent === undefined
|
|
881
|
-
? {}
|
|
882
|
-
: { reasoning_content: message.reasoningContent }),
|
|
883
|
-
...(message.role !== "assistant" || (message.toolCalls?.length ?? 0) === 0
|
|
884
|
-
? {}
|
|
885
|
-
: {
|
|
886
|
-
tool_calls: message.toolCalls.map((call) => ({
|
|
887
|
-
id: call.id,
|
|
888
|
-
type: "function",
|
|
889
|
-
function: {
|
|
890
|
-
name: call.name,
|
|
891
|
-
arguments: JSON.stringify(call.arguments),
|
|
892
|
-
},
|
|
893
|
-
})),
|
|
894
|
-
}),
|
|
895
1183
|
};
|
|
896
1184
|
}
|
|
897
|
-
function
|
|
898
|
-
|
|
899
|
-
|
|
1185
|
+
function anthropicSettingMessages(source) {
|
|
1186
|
+
const messages = [];
|
|
1187
|
+
const cacheableUserIndex = settingPrefixUserIndex(source);
|
|
1188
|
+
let toolResults = [];
|
|
1189
|
+
const flushToolResults = () => {
|
|
1190
|
+
if (toolResults.length === 0)
|
|
1191
|
+
return;
|
|
1192
|
+
messages.push({ role: "user", content: toolResults });
|
|
1193
|
+
toolResults = [];
|
|
1194
|
+
};
|
|
1195
|
+
for (const [index, message] of source.entries()) {
|
|
1196
|
+
if (message.role === "system")
|
|
1197
|
+
continue;
|
|
1198
|
+
if (message.role === "tool") {
|
|
1199
|
+
toolResults.push({
|
|
1200
|
+
type: "tool_result",
|
|
1201
|
+
tool_use_id: message.toolCallId,
|
|
1202
|
+
content: message.content,
|
|
1203
|
+
...(message.isError === true ? { is_error: true } : {}),
|
|
1204
|
+
});
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
flushToolResults();
|
|
1208
|
+
if (message.role === "assistant") {
|
|
1209
|
+
if (message.providerState?.protocol !== "anthropic_messages" ||
|
|
1210
|
+
!validAnthropicContent(message.providerState.content))
|
|
1211
|
+
throw continuationError("Anthropic Messages setting improvement");
|
|
1212
|
+
messages.push({
|
|
1213
|
+
role: "assistant",
|
|
1214
|
+
content: cloneProviderValue(message.providerState.content),
|
|
1215
|
+
});
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
messages.push({
|
|
900
1219
|
role: "user",
|
|
901
|
-
content:
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1220
|
+
content: index === cacheableUserIndex
|
|
1221
|
+
? [
|
|
1222
|
+
{
|
|
1223
|
+
type: "text",
|
|
1224
|
+
text: message.content,
|
|
1225
|
+
cache_control: { type: "ephemeral" },
|
|
1226
|
+
},
|
|
1227
|
+
]
|
|
1228
|
+
: message.content,
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
flushToolResults();
|
|
1232
|
+
return messages;
|
|
1233
|
+
}
|
|
1234
|
+
function responsesSettingInput(connection, messages) {
|
|
1235
|
+
const translated = messages.flatMap(responsesSettingMessage);
|
|
1236
|
+
if (connection.dialect !== "cliproxyapi")
|
|
1237
|
+
return translated;
|
|
1238
|
+
return attachCacheControlAtIndex(attachCacheControlToLastRole(translated, "system"), settingPrefixUserIndex(messages));
|
|
1239
|
+
}
|
|
1240
|
+
function settingPrefixUserIndex(messages) {
|
|
1241
|
+
const firstAssistant = messages.findIndex(({ role }) => role === "assistant");
|
|
1242
|
+
const end = firstAssistant < 0 ? messages.length : firstAssistant;
|
|
1243
|
+
let target = -1;
|
|
1244
|
+
for (let index = 0; index < end; index += 1)
|
|
1245
|
+
if (messages[index]?.role === "user")
|
|
1246
|
+
target = index;
|
|
1247
|
+
return target;
|
|
1248
|
+
}
|
|
1249
|
+
function attachCacheControlAtIndex(messages, index) {
|
|
1250
|
+
const result = cloneProviderValue(messages);
|
|
1251
|
+
if (index < 0 || !isRecord(result[index]))
|
|
1252
|
+
return result;
|
|
1253
|
+
result[index] = {
|
|
1254
|
+
...result[index],
|
|
1255
|
+
cache_control: { type: "ephemeral" },
|
|
1256
|
+
};
|
|
1257
|
+
return result;
|
|
925
1258
|
}
|
|
926
1259
|
function responsesSettingMessage(message) {
|
|
927
1260
|
if (message.role === "tool")
|
|
@@ -933,14 +1266,32 @@ function responsesSettingMessage(message) {
|
|
|
933
1266
|
},
|
|
934
1267
|
];
|
|
935
1268
|
if (message.role === "assistant" &&
|
|
936
|
-
message.providerState?.protocol === "openai_responses"
|
|
1269
|
+
message.providerState?.protocol === "openai_responses" &&
|
|
1270
|
+
validResponsesOutput(message.providerState.output))
|
|
937
1271
|
return cloneResponseOutput(message.providerState.output);
|
|
938
1272
|
if (message.role === "assistant")
|
|
939
|
-
|
|
1273
|
+
throw continuationError("OpenAI Responses setting improvement");
|
|
940
1274
|
return [{ role: message.role, content: message.content }];
|
|
941
1275
|
}
|
|
942
|
-
function responsesBootstrapMessage(message) {
|
|
943
|
-
return {
|
|
1276
|
+
function responsesBootstrapMessage(message, preserveCacheControl = false) {
|
|
1277
|
+
return {
|
|
1278
|
+
role: message.role,
|
|
1279
|
+
content: preserveCacheControl && Array.isArray(message.content)
|
|
1280
|
+
? message.content.flatMap((item) => {
|
|
1281
|
+
if (!isRecord(item) || typeof item.text !== "string")
|
|
1282
|
+
return [];
|
|
1283
|
+
return [
|
|
1284
|
+
{
|
|
1285
|
+
type: "input_text",
|
|
1286
|
+
text: item.text,
|
|
1287
|
+
...(isRecord(item.cache_control)
|
|
1288
|
+
? { cache_control: cloneProviderValue(item.cache_control) }
|
|
1289
|
+
: {}),
|
|
1290
|
+
},
|
|
1291
|
+
];
|
|
1292
|
+
})
|
|
1293
|
+
: providerText(message.content),
|
|
1294
|
+
};
|
|
944
1295
|
}
|
|
945
1296
|
function providerText(content) {
|
|
946
1297
|
if (typeof content === "string")
|
|
@@ -953,50 +1304,71 @@ function providerText(content) {
|
|
|
953
1304
|
}
|
|
954
1305
|
return JSON.stringify(content) ?? "";
|
|
955
1306
|
}
|
|
956
|
-
function
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1307
|
+
function validChatAssistantMessage(value) {
|
|
1308
|
+
if (!isRecord(value) || value.role !== "assistant")
|
|
1309
|
+
return false;
|
|
1310
|
+
if (value.content !== undefined &&
|
|
1311
|
+
value.content !== null &&
|
|
1312
|
+
typeof value.content !== "string")
|
|
1313
|
+
return false;
|
|
1314
|
+
if (value.reasoning_content !== undefined &&
|
|
1315
|
+
value.reasoning_content !== null &&
|
|
1316
|
+
typeof value.reasoning_content !== "string")
|
|
1317
|
+
return false;
|
|
1318
|
+
return (value.tool_calls === undefined ||
|
|
1319
|
+
(Array.isArray(value.tool_calls) &&
|
|
1320
|
+
value.tool_calls.every((call) => isRecord(call) &&
|
|
1321
|
+
typeof call.id === "string" &&
|
|
1322
|
+
isRecord(call.function) &&
|
|
1323
|
+
typeof call.function.name === "string" &&
|
|
1324
|
+
typeof call.function.arguments === "string")));
|
|
1325
|
+
}
|
|
1326
|
+
function validAnthropicContent(value) {
|
|
1327
|
+
return (Array.isArray(value) &&
|
|
1328
|
+
value.every((item) => {
|
|
1329
|
+
if (!isRecord(item) || typeof item.type !== "string")
|
|
1330
|
+
return false;
|
|
1331
|
+
if (item.type === "text")
|
|
1332
|
+
return typeof item.text === "string";
|
|
1333
|
+
if (item.type === "thinking")
|
|
1334
|
+
return (typeof item.thinking === "string" &&
|
|
1335
|
+
(item.signature === undefined || typeof item.signature === "string"));
|
|
1336
|
+
if (item.type === "redacted_thinking")
|
|
1337
|
+
return typeof item.data === "string";
|
|
1338
|
+
if (item.type === "tool_use")
|
|
1339
|
+
return (typeof item.id === "string" &&
|
|
1340
|
+
typeof item.name === "string" &&
|
|
1341
|
+
isRecord(item.input));
|
|
1342
|
+
return true;
|
|
1343
|
+
}));
|
|
1344
|
+
}
|
|
1345
|
+
function validResponsesOutput(value) {
|
|
1346
|
+
return (Array.isArray(value) &&
|
|
1347
|
+
value.every((item) => {
|
|
1348
|
+
if (!isRecord(item) || typeof item.type !== "string")
|
|
1349
|
+
return false;
|
|
1350
|
+
if (item.type === "message")
|
|
1351
|
+
return Array.isArray(item.content);
|
|
1352
|
+
if (item.type === "function_call")
|
|
1353
|
+
return (typeof item.call_id === "string" &&
|
|
1354
|
+
typeof item.name === "string" &&
|
|
1355
|
+
typeof item.arguments === "string");
|
|
1356
|
+
if (item.type === "reasoning")
|
|
1357
|
+
return ((item.summary === undefined || Array.isArray(item.summary)) &&
|
|
1358
|
+
(item.content === undefined ||
|
|
1359
|
+
item.content === null ||
|
|
1360
|
+
Array.isArray(item.content)) &&
|
|
1361
|
+
(item.encrypted_content === undefined ||
|
|
1362
|
+
item.encrypted_content === null ||
|
|
1363
|
+
typeof item.encrypted_content === "string"));
|
|
1364
|
+
return true;
|
|
1365
|
+
}));
|
|
1366
|
+
}
|
|
1367
|
+
function continuationError(protocol) {
|
|
1368
|
+
return new ModelHostContinuationError(`${protocol} continuation payload is missing or incompatible; start a fresh model context instead of rebuilding it from parsed text, reasoning, or tool calls`);
|
|
966
1369
|
}
|
|
967
1370
|
async function openAIResponsesRequest(input) {
|
|
968
|
-
const
|
|
969
|
-
const toolChoice = input.hasFrozenToolPolicy === true && allowedTools.length === 0
|
|
970
|
-
? "none"
|
|
971
|
-
: input.hasFrozenToolPolicy === true &&
|
|
972
|
-
input.toolStrategy === "native_allowed_subset"
|
|
973
|
-
? {
|
|
974
|
-
type: "allowed_tools",
|
|
975
|
-
mode: "auto",
|
|
976
|
-
tools: allowedTools.map((name) => ({ type: "function", name })),
|
|
977
|
-
}
|
|
978
|
-
: "auto";
|
|
979
|
-
const body = {
|
|
980
|
-
model: input.connection.modelId,
|
|
981
|
-
input: input.input,
|
|
982
|
-
...(input.tools.length === 0
|
|
983
|
-
? input.hasFrozenToolPolicy === true && allowedTools.length === 0
|
|
984
|
-
? { tool_choice: "none" }
|
|
985
|
-
: {}
|
|
986
|
-
: {
|
|
987
|
-
tools: input.tools.map((tool) => ({
|
|
988
|
-
type: "function",
|
|
989
|
-
name: tool.name,
|
|
990
|
-
description: tool.description,
|
|
991
|
-
parameters: tool.parameters,
|
|
992
|
-
})),
|
|
993
|
-
tool_choice: toolChoice,
|
|
994
|
-
}),
|
|
995
|
-
max_output_tokens: input.maxOutputTokens,
|
|
996
|
-
store: false,
|
|
997
|
-
include: ["reasoning.encrypted_content"],
|
|
998
|
-
stream: true,
|
|
999
|
-
};
|
|
1371
|
+
const body = responsesRequestBody(input);
|
|
1000
1372
|
await trace(`${input.tracePrefix}_request`, body);
|
|
1001
1373
|
const bodyJson = JSON.stringify(body);
|
|
1002
1374
|
const url = providerUrl(input.connection);
|
|
@@ -1021,13 +1393,7 @@ async function openAIResponsesRequest(input) {
|
|
|
1021
1393
|
if (!isRecord(payload) || !Array.isArray(payload.output))
|
|
1022
1394
|
throw unknownProviderResponse("Responses API");
|
|
1023
1395
|
const output = cloneResponseOutput(payload.output);
|
|
1024
|
-
if (
|
|
1025
|
-
typeof item.type !== "string" ||
|
|
1026
|
-
(item.type === "message" && !Array.isArray(item.content)) ||
|
|
1027
|
-
(item.type === "function_call" &&
|
|
1028
|
-
(typeof item.call_id !== "string" ||
|
|
1029
|
-
typeof item.name !== "string" ||
|
|
1030
|
-
typeof item.arguments !== "string"))))
|
|
1396
|
+
if (!validResponsesOutput(output))
|
|
1031
1397
|
throw unknownProviderResponse("Responses API");
|
|
1032
1398
|
const text = output
|
|
1033
1399
|
.flatMap((item) => {
|
|
@@ -1042,6 +1408,7 @@ async function openAIResponsesRequest(input) {
|
|
|
1042
1408
|
: []);
|
|
1043
1409
|
})
|
|
1044
1410
|
.join("");
|
|
1411
|
+
const reasoningContent = responsesReturnedReasoning(output);
|
|
1045
1412
|
const toolCalls = output.flatMap((item) => {
|
|
1046
1413
|
if (!isRecord(item) ||
|
|
1047
1414
|
item.type !== "function_call" ||
|
|
@@ -1057,18 +1424,61 @@ async function openAIResponsesRequest(input) {
|
|
|
1057
1424
|
},
|
|
1058
1425
|
];
|
|
1059
1426
|
});
|
|
1427
|
+
const stopReason = responsesStopReason(payload);
|
|
1060
1428
|
return {
|
|
1061
1429
|
...(text.length === 0 ? {} : { text }),
|
|
1430
|
+
...(reasoningContent.length === 0 ? {} : { reasoningContent }),
|
|
1062
1431
|
providerState: {
|
|
1063
1432
|
protocol: "openai_responses",
|
|
1064
1433
|
output,
|
|
1065
1434
|
...(typeof payload.id === "string" ? { responseId: payload.id } : {}),
|
|
1066
1435
|
},
|
|
1067
1436
|
usage: normalizeResponsesUsage(payload.usage),
|
|
1437
|
+
...(stopReason === undefined ? {} : { stopReason }),
|
|
1068
1438
|
toolCalls,
|
|
1069
1439
|
};
|
|
1070
1440
|
});
|
|
1071
1441
|
}
|
|
1442
|
+
function responsesStopReason(payload) {
|
|
1443
|
+
const status = typeof payload.status === "string" && payload.status !== ""
|
|
1444
|
+
? payload.status
|
|
1445
|
+
: undefined;
|
|
1446
|
+
const details = isRecord(payload.incomplete_details)
|
|
1447
|
+
? payload.incomplete_details
|
|
1448
|
+
: undefined;
|
|
1449
|
+
const reason = details !== undefined &&
|
|
1450
|
+
typeof details.reason === "string" &&
|
|
1451
|
+
details.reason !== ""
|
|
1452
|
+
? details.reason
|
|
1453
|
+
: undefined;
|
|
1454
|
+
if (status !== undefined && reason !== undefined)
|
|
1455
|
+
return `${status}:${reason}`;
|
|
1456
|
+
return reason ?? status;
|
|
1457
|
+
}
|
|
1458
|
+
function responsesReturnedReasoning(output) {
|
|
1459
|
+
return output
|
|
1460
|
+
.flatMap((item) => {
|
|
1461
|
+
if (!isRecord(item) || item.type !== "reasoning")
|
|
1462
|
+
return [];
|
|
1463
|
+
const summary = Array.isArray(item.summary)
|
|
1464
|
+
? item.summary.flatMap((part) => isRecord(part) &&
|
|
1465
|
+
part.type === "summary_text" &&
|
|
1466
|
+
typeof part.text === "string"
|
|
1467
|
+
? [part.text]
|
|
1468
|
+
: [])
|
|
1469
|
+
: [];
|
|
1470
|
+
if (summary.length > 0)
|
|
1471
|
+
return summary;
|
|
1472
|
+
return Array.isArray(item.content)
|
|
1473
|
+
? item.content.flatMap((part) => isRecord(part) &&
|
|
1474
|
+
part.type === "reasoning_text" &&
|
|
1475
|
+
typeof part.text === "string"
|
|
1476
|
+
? [part.text]
|
|
1477
|
+
: [])
|
|
1478
|
+
: [];
|
|
1479
|
+
})
|
|
1480
|
+
.join("");
|
|
1481
|
+
}
|
|
1072
1482
|
function cloneResponseOutput(value) {
|
|
1073
1483
|
if (!Array.isArray(value))
|
|
1074
1484
|
throw new Error("Responses output must be an array");
|
|
@@ -1094,7 +1504,8 @@ function normalizeChatUsage(value) {
|
|
|
1094
1504
|
: {};
|
|
1095
1505
|
const inputTokens = providerNumber(usage.prompt_tokens);
|
|
1096
1506
|
const cacheReadTokens = providerNumber(promptDetails.cached_tokens);
|
|
1097
|
-
const cacheWriteTokens = providerNumber(promptDetails.cache_write_tokens)
|
|
1507
|
+
const cacheWriteTokens = providerNumber(promptDetails.cache_write_tokens) ??
|
|
1508
|
+
providerNumber(promptDetails.cached_creation_tokens);
|
|
1098
1509
|
return usageRecord({
|
|
1099
1510
|
inputTokens,
|
|
1100
1511
|
uncachedInputTokens: subtractCachedInput(inputTokens, cacheReadTokens, cacheWriteTokens),
|
|
@@ -1107,6 +1518,9 @@ function normalizeChatUsage(value) {
|
|
|
1107
1518
|
}
|
|
1108
1519
|
function normalizeAnthropicUsage(value) {
|
|
1109
1520
|
const usage = isRecord(value) ? value : {};
|
|
1521
|
+
const outputDetails = isRecord(usage.output_tokens_details)
|
|
1522
|
+
? usage.output_tokens_details
|
|
1523
|
+
: {};
|
|
1110
1524
|
const uncachedInputTokens = providerNumber(usage.input_tokens);
|
|
1111
1525
|
const cacheReadTokens = providerNumber(usage.cache_read_input_tokens);
|
|
1112
1526
|
const cacheWriteTokens = providerNumber(usage.cache_creation_input_tokens);
|
|
@@ -1117,7 +1531,8 @@ function normalizeAnthropicUsage(value) {
|
|
|
1117
1531
|
uncachedInputTokens,
|
|
1118
1532
|
cacheReadTokens,
|
|
1119
1533
|
cacheWriteTokens,
|
|
1120
|
-
reasoningTokens:
|
|
1534
|
+
reasoningTokens: providerNumber(outputDetails.thinking_tokens) ??
|
|
1535
|
+
providerNumber(usage.thinking_tokens),
|
|
1121
1536
|
outputTokens,
|
|
1122
1537
|
totalTokens: inputTokens === null || outputTokens === null
|
|
1123
1538
|
? null
|
|
@@ -1134,7 +1549,8 @@ function normalizeResponsesUsage(value) {
|
|
|
1134
1549
|
: {};
|
|
1135
1550
|
const inputTokens = providerNumber(usage.input_tokens);
|
|
1136
1551
|
const cacheReadTokens = providerNumber(inputDetails.cached_tokens);
|
|
1137
|
-
const cacheWriteTokens = providerNumber(inputDetails.cache_write_tokens)
|
|
1552
|
+
const cacheWriteTokens = providerNumber(inputDetails.cache_write_tokens) ??
|
|
1553
|
+
providerNumber(inputDetails.cached_creation_tokens);
|
|
1138
1554
|
return usageRecord({
|
|
1139
1555
|
inputTokens,
|
|
1140
1556
|
uncachedInputTokens: subtractCachedInput(inputTokens, cacheReadTokens, cacheWriteTokens),
|