chainlesschain 0.166.131 → 0.166.133
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 +46 -18
- package/package.json +1 -1
- package/src/assets/linux-subreaper/linux-arm64/manifest.json +1 -1
- package/src/assets/linux-subreaper/linux-x64/manifest.json +1 -1
- package/src/data/changelog.json +26 -0
- package/src/gateways/ws/llm-chat-protocol.js +1 -1
- package/src/gateways/ws/personal-data-hub-protocol.js +27 -11
- package/src/gateways/ws/session-core-protocol.js +22 -17
- package/src/gateways/ws/session-protocol.js +19 -8
- package/src/gateways/ws/ws-agent-handler.js +30 -0
- package/src/lib/chat-core.js +40 -35
- package/src/lib/context-memory-kernel/message-adapter.js +27 -6
- package/src/lib/context-memory-kernel/provider-context.js +48 -3
- package/src/lib/evolution/agent-model-projection.js +0 -1
- package/src/lib/evolution/governed-hub-llm.js +5 -1
- package/src/lib/evolution/governed-hub-resolver.js +15 -6
- package/src/lib/evolution/governed-hub-skill.js +2 -0
- package/src/lib/evolution/optional-ui-model-turn.js +13 -2
- package/src/lib/llm-config-defaults.js +17 -5
- package/src/lib/llm-connection-config.js +10 -2
- package/src/lib/llm-connection-probe.js +5 -2
- package/src/lib/llm-connection-resolution.js +7 -4
- package/src/lib/llm-providers.js +24 -11
- package/src/lib/openai-responses.js +2 -1
- package/src/lib/personal-data-hub-wiring.js +69 -36
- package/src/lib/provider-protocol.js +35 -3
- package/src/lib/provider-stream.js +6 -4
- package/src/lib/runnable-provider.js +4 -0
- package/src/lib/ws-chat-handler.js +22 -9
- package/src/runtime/agent-core.js +201 -15
- package/src/runtime/message-roles.js +67 -3
- package/src/runtime/provider-stream-state.js +35 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
1
2
|
import {
|
|
2
3
|
canonicalDigest,
|
|
3
4
|
canonicalJson,
|
|
@@ -205,6 +206,17 @@ export function messagesToContextItems(messagesInput, options = {}) {
|
|
|
205
206
|
? [...options.allowedSinks]
|
|
206
207
|
: DEFAULT_ALLOWED_SINKS;
|
|
207
208
|
const items = [];
|
|
209
|
+
const requiredUser = options.requiredUserMessage;
|
|
210
|
+
if (
|
|
211
|
+
requiredUser != null &&
|
|
212
|
+
(!Number.isSafeInteger(requiredUser.index) ||
|
|
213
|
+
requiredUser.index < 0 ||
|
|
214
|
+
requiredUser.index >= messages.length ||
|
|
215
|
+
requiredUser.message?.role !== "user" ||
|
|
216
|
+
!isDeepStrictEqual(messages[requiredUser.index], requiredUser.message))
|
|
217
|
+
) {
|
|
218
|
+
throw new TypeError("Invalid current-user task binding");
|
|
219
|
+
}
|
|
208
220
|
const trustedSystemIndexes = new Set(
|
|
209
221
|
Array.isArray(options.trustedSystemIndexes)
|
|
210
222
|
? options.trustedSystemIndexes.filter(Number.isSafeInteger)
|
|
@@ -289,11 +301,20 @@ export function messagesToContextItems(messagesInput, options = {}) {
|
|
|
289
301
|
const lastIndex = items.findLastIndex(
|
|
290
302
|
(item) => item.kind !== "system-policy",
|
|
291
303
|
);
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
304
|
+
for (let index = 0; index < items.length; index++) {
|
|
305
|
+
const item = items[index];
|
|
306
|
+
if (
|
|
307
|
+
index !== lastIndex &&
|
|
308
|
+
item.sourceRef.eventSequence !== requiredUser?.index
|
|
309
|
+
)
|
|
310
|
+
continue;
|
|
311
|
+
const payload = JSON.parse(loadBundleContent(item, sessionId));
|
|
312
|
+
if (
|
|
313
|
+
payload.messages.at(-1)?.role === "user" &&
|
|
314
|
+
(index === lastIndex ||
|
|
315
|
+
item.sourceRef.eventSequence === requiredUser?.index)
|
|
316
|
+
) {
|
|
317
|
+
const protectedItem = { ...item };
|
|
297
318
|
delete protectedItem.digest;
|
|
298
319
|
protectedItem.kind = "task-state";
|
|
299
320
|
protectedItem.priority = 900_000;
|
|
@@ -301,7 +322,7 @@ export function messagesToContextItems(messagesInput, options = {}) {
|
|
|
301
322
|
taskState: "waiting",
|
|
302
323
|
requiredForRecovery: true,
|
|
303
324
|
};
|
|
304
|
-
items[
|
|
325
|
+
items[index] = normalizeContextItem(protectedItem);
|
|
305
326
|
}
|
|
306
327
|
}
|
|
307
328
|
return items;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
1
2
|
import {
|
|
2
3
|
canonicalDigest,
|
|
3
4
|
canonicalJson,
|
|
@@ -203,6 +204,40 @@ export async function prepareCanonicalProviderContext(
|
|
|
203
204
|
};
|
|
204
205
|
}
|
|
205
206
|
|
|
207
|
+
// Resolve against the final provider projection, never a pre-projection index.
|
|
208
|
+
// Exact role/content/metadata matching prevents a transformed tool result or
|
|
209
|
+
// an ambiguous duplicate from taking the original user's task-state binding.
|
|
210
|
+
const taskBinding = options.contextMemoryRequiredUserBinding;
|
|
211
|
+
const requiredUserMessage = taskBinding?.message;
|
|
212
|
+
function resolveRequiredUserIndex() {
|
|
213
|
+
if (requiredUserMessage == null) return null;
|
|
214
|
+
const identities = messages.flatMap((message, index) =>
|
|
215
|
+
message === taskBinding.original ? [index] : [],
|
|
216
|
+
);
|
|
217
|
+
const candidates = identities.length
|
|
218
|
+
? identities
|
|
219
|
+
: taskBinding.allowContentRelocation === true
|
|
220
|
+
? messages.map((_, index) => index)
|
|
221
|
+
: [];
|
|
222
|
+
const matches = candidates.filter((index) => {
|
|
223
|
+
const message = messages[index];
|
|
224
|
+
return (
|
|
225
|
+
message?.role === "user" &&
|
|
226
|
+
requiredUserMessage.role === "user" &&
|
|
227
|
+
isDeepStrictEqual(message, requiredUserMessage)
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
if (matches.length !== 1 || identities.length > 1) {
|
|
231
|
+
const error = new Error(
|
|
232
|
+
"The current user task was lost, changed or duplicated in provider context",
|
|
233
|
+
);
|
|
234
|
+
error.code = "CC_AGENT_TASK_CONTEXT_LOST";
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
return matches[0];
|
|
238
|
+
}
|
|
239
|
+
let requiredUserIndex = resolveRequiredUserIndex();
|
|
240
|
+
|
|
206
241
|
// Review can exclude recalled author/session data while retaining the exact
|
|
207
242
|
// canonical context, tool and output-token planning boundary.
|
|
208
243
|
const recallEnabled = options.contextMemoryRecall !== false;
|
|
@@ -211,9 +246,9 @@ export async function prepareCanonicalProviderContext(
|
|
|
211
246
|
: null;
|
|
212
247
|
const sink = String(options.contextMemorySink || `provider.${provider}`);
|
|
213
248
|
const scopeAdmissions = defaultAdmissions(sessionId, options);
|
|
214
|
-
const lastUser =
|
|
215
|
-
|
|
216
|
-
.find((message) => message?.role === "user");
|
|
249
|
+
const lastUser =
|
|
250
|
+
requiredUserMessage ||
|
|
251
|
+
[...messages].reverse().find((message) => message?.role === "user");
|
|
217
252
|
const query =
|
|
218
253
|
typeof lastUser?.content === "string" && lastUser.content.trim()
|
|
219
254
|
? lastUser.content.trim().slice(0, 32 * 1024)
|
|
@@ -254,6 +289,9 @@ export async function prepareCanonicalProviderContext(
|
|
|
254
289
|
}),
|
|
255
290
|
};
|
|
256
291
|
options.signal?.throwIfAborted();
|
|
292
|
+
// Recall awaits host and storage work. Revalidate the captured task and its
|
|
293
|
+
// current position before image transformation and immutable item creation.
|
|
294
|
+
requiredUserIndex = resolveRequiredUserIndex();
|
|
257
295
|
const imageTransport = captureContextImageTransport(messages);
|
|
258
296
|
const messageItems = adjustContextImageTokenEstimates(
|
|
259
297
|
messagesToContextItems(imageTransport.messages, {
|
|
@@ -261,6 +299,13 @@ export async function prepareCanonicalProviderContext(
|
|
|
261
299
|
allowedSinks: [sink],
|
|
262
300
|
trustCurrentSystemPolicy: true,
|
|
263
301
|
trustedSystemIndexes: options.contextMemoryTrustedSystemIndexes,
|
|
302
|
+
requiredUserMessage:
|
|
303
|
+
requiredUserIndex === null
|
|
304
|
+
? null
|
|
305
|
+
: {
|
|
306
|
+
index: requiredUserIndex,
|
|
307
|
+
message: imageTransport.messages[requiredUserIndex],
|
|
308
|
+
},
|
|
264
309
|
allowExternalization: false,
|
|
265
310
|
}),
|
|
266
311
|
imageTransport.captures,
|
|
@@ -264,7 +264,10 @@ export function createGovernedHubLlm(llm, factory, options = {}, sdkPorts) {
|
|
|
264
264
|
"A Hub model client with declared locality is required",
|
|
265
265
|
);
|
|
266
266
|
}
|
|
267
|
-
if (
|
|
267
|
+
if (
|
|
268
|
+
typeof factory !== "function" &&
|
|
269
|
+
!(factory === null && options.evolutionDisabled === true)
|
|
270
|
+
)
|
|
268
271
|
throw new TypeError("A Hub evolution factory is required");
|
|
269
272
|
if (options.onFailure != null && typeof options.onFailure !== "function")
|
|
270
273
|
throw new TypeError("onFailure must be a function");
|
|
@@ -298,6 +301,7 @@ export function createGovernedHubLlm(llm, factory, options = {}, sdkPorts) {
|
|
|
298
301
|
mode: "hub-analysis",
|
|
299
302
|
messages,
|
|
300
303
|
signal: chatOptions.signal,
|
|
304
|
+
evolutionDisabled: options.evolutionDisabled === true,
|
|
301
305
|
});
|
|
302
306
|
},
|
|
303
307
|
async transport(messages, chatOptions = {}) {
|
|
@@ -77,8 +77,16 @@ function captureEmbeddingTransport(stage, sdk) {
|
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
export function createGovernedHubResolver(
|
|
81
|
-
|
|
80
|
+
export function createGovernedHubResolver(
|
|
81
|
+
ports,
|
|
82
|
+
factory,
|
|
83
|
+
sdkPorts,
|
|
84
|
+
options = {},
|
|
85
|
+
) {
|
|
86
|
+
if (
|
|
87
|
+
typeof factory !== "function" &&
|
|
88
|
+
!(factory === null && options.evolutionDisabled === true)
|
|
89
|
+
)
|
|
82
90
|
throw new TypeError("A resolver evolution factory is required");
|
|
83
91
|
const {
|
|
84
92
|
resolver,
|
|
@@ -102,13 +110,14 @@ export function createGovernedHubResolver(ports, factory, sdkPorts) {
|
|
|
102
110
|
const turn = await prepareGovernedModelTurn(factory, {
|
|
103
111
|
mode: "hub-embedding",
|
|
104
112
|
messages: [{ role: "user", content: profile }],
|
|
113
|
+
evolutionDisabled: options.evolutionDisabled === true,
|
|
105
114
|
});
|
|
106
115
|
// Chat projection prepends host provenance. Embed only the projected
|
|
107
116
|
// profile: per-Run evidence IDs must not change semantic similarity.
|
|
108
|
-
const profileMessage = turn.messages[1];
|
|
117
|
+
const profileMessage = turn.messages[turn.governed ? 1 : 0];
|
|
109
118
|
if (
|
|
110
|
-
turn.messages.length !== 2 ||
|
|
111
|
-
turn.messages[0].role !== "system" ||
|
|
119
|
+
turn.messages.length !== (turn.governed ? 2 : 1) ||
|
|
120
|
+
(turn.governed && turn.messages[0].role !== "system") ||
|
|
112
121
|
profileMessage?.role !== "user" ||
|
|
113
122
|
typeof profileMessage.content !== "string"
|
|
114
123
|
)
|
|
@@ -133,7 +142,7 @@ export function createGovernedHubResolver(ports, factory, sdkPorts) {
|
|
|
133
142
|
const model =
|
|
134
143
|
resolver._llmStage &&
|
|
135
144
|
new LLMStage({
|
|
136
|
-
llm: createGovernedHubLlm(llm, factory,
|
|
145
|
+
llm: createGovernedHubLlm(llm, factory, options, sdk),
|
|
137
146
|
acceptNonLocal: false,
|
|
138
147
|
});
|
|
139
148
|
return new EntityResolver({
|
|
@@ -9,6 +9,7 @@ export async function runGovernedHubSkill(
|
|
|
9
9
|
name,
|
|
10
10
|
options,
|
|
11
11
|
sdkPorts,
|
|
12
|
+
modelTurnOptions = {},
|
|
12
13
|
) {
|
|
13
14
|
let failure;
|
|
14
15
|
let failed = false;
|
|
@@ -16,6 +17,7 @@ export async function runGovernedHubSkill(
|
|
|
16
17
|
hub.llm,
|
|
17
18
|
factory,
|
|
18
19
|
{
|
|
20
|
+
evolutionDisabled: modelTurnOptions.evolutionDisabled === true,
|
|
19
21
|
onFailure(error) {
|
|
20
22
|
failure = error;
|
|
21
23
|
failed = true;
|
|
@@ -12,9 +12,20 @@ export async function loadConfiguredUiEvolutionFactory(options = {}) {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export async function resolveServerEvolutionFactory(server) {
|
|
15
|
-
return typeof server
|
|
15
|
+
return typeof server?.resolveEvolutionCompositionFactory === "function"
|
|
16
16
|
? server.resolveEvolutionCompositionFactory()
|
|
17
|
-
: (server
|
|
17
|
+
: (server?.evolutionCompositionFactory ?? null);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Apply the host's current UI policy for every turn, including old sessions. */
|
|
21
|
+
export async function prepareServerUiModelTurn(server, context) {
|
|
22
|
+
if (typeof server?.prepareUiModelTurn === "function") {
|
|
23
|
+
return server.prepareUiModelTurn(context);
|
|
24
|
+
}
|
|
25
|
+
return prepareGovernedModelTurn(
|
|
26
|
+
await resolveServerEvolutionFactory(server),
|
|
27
|
+
context,
|
|
28
|
+
);
|
|
18
29
|
}
|
|
19
30
|
|
|
20
31
|
export async function prepareConfiguredUiModelTurn(context, options = {}) {
|
|
@@ -35,7 +35,12 @@
|
|
|
35
35
|
* so an intentionally-custom endpoint is left untouched.
|
|
36
36
|
*/
|
|
37
37
|
export async function reconcileConfigLlmProvider(cfgLlm = {}) {
|
|
38
|
-
if (
|
|
38
|
+
if (
|
|
39
|
+
!cfgLlm ||
|
|
40
|
+
!cfgLlm.provider ||
|
|
41
|
+
!cfgLlm.baseUrl ||
|
|
42
|
+
cfgLlm.provider === "custom"
|
|
43
|
+
) {
|
|
39
44
|
return { llm: cfgLlm, changed: false };
|
|
40
45
|
}
|
|
41
46
|
const { inferProviderFromBaseUrl, modelForeignToProvider } = await import(
|
|
@@ -71,22 +76,29 @@ export function applyConfigLlmDefaults(options = {}, cfgLlm = {}, opts = {}) {
|
|
|
71
76
|
// credentials are exactly right. Mixing config into a DIFFERENT explicit
|
|
72
77
|
// provider is still avoided (handled by the early return below).
|
|
73
78
|
if (options.provider && configMatchesTarget) {
|
|
74
|
-
if (!options.baseUrl && cfgLlm.baseUrl)
|
|
79
|
+
if (!options.baseUrl && cfgLlm.baseUrl)
|
|
80
|
+
options.baseUrl = connection.baseUrl;
|
|
75
81
|
if (options.serviceTier == null && cfgLlm.serviceTier != null)
|
|
76
82
|
options.serviceTier = cfgLlm.serviceTier;
|
|
77
|
-
if (!options.apiKey && connection.apiKey)
|
|
83
|
+
if (!options.apiKey && connection.apiKey)
|
|
84
|
+
options.apiKey = connection.apiKey;
|
|
78
85
|
if (!options.model && !opts.explicitModel && cfgLlm.model) {
|
|
79
86
|
options.model = cfgLlm.model;
|
|
80
87
|
}
|
|
81
88
|
return options;
|
|
82
89
|
}
|
|
83
90
|
if (options.provider) {
|
|
84
|
-
if (!options.apiKey && connection.apiKey)
|
|
91
|
+
if (!options.apiKey && connection.apiKey)
|
|
92
|
+
options.apiKey = connection.apiKey;
|
|
85
93
|
return options;
|
|
86
94
|
}
|
|
87
95
|
if (!cfgLlm.provider) return options;
|
|
88
96
|
options.provider = connection.provider;
|
|
89
|
-
if (
|
|
97
|
+
if (
|
|
98
|
+
configMatchesTarget &&
|
|
99
|
+
options.serviceTier == null &&
|
|
100
|
+
cfgLlm.serviceTier != null
|
|
101
|
+
)
|
|
90
102
|
options.serviceTier = cfgLlm.serviceTier;
|
|
91
103
|
if (opts.explicitModel) {
|
|
92
104
|
options.model = opts.explicitModel;
|
|
@@ -31,7 +31,10 @@ export function normalizeLlmConnection(input) {
|
|
|
31
31
|
if (value[key].length > (key === "apiKey" ? 8192 : 2048))
|
|
32
32
|
throw new Error(`${key} is too long`);
|
|
33
33
|
}
|
|
34
|
-
if (
|
|
34
|
+
if (
|
|
35
|
+
value.provider !== "custom" &&
|
|
36
|
+
!Object.hasOwn(BUILT_IN_PROVIDERS, value.provider)
|
|
37
|
+
)
|
|
35
38
|
throw new Error("Choose a supported provider/protocol");
|
|
36
39
|
if (!value.model || /[\r\n\0]/.test(value.model + value.visionModel))
|
|
37
40
|
throw new Error("A valid model name is required");
|
|
@@ -84,7 +87,7 @@ export function saveLlmConnection(input, { storage = "auto" } = {}) {
|
|
|
84
87
|
current.baseUrl || BUILT_IN_PROVIDERS[current.provider]?.baseUrl || "",
|
|
85
88
|
).replace(/\/+$/, "") === value.baseUrl;
|
|
86
89
|
if (
|
|
87
|
-
value.provider
|
|
90
|
+
!["ollama", "custom"].includes(value.provider) &&
|
|
88
91
|
!value.apiKey &&
|
|
89
92
|
(!sameEndpoint || !current.apiKey)
|
|
90
93
|
)
|
|
@@ -98,6 +101,11 @@ export function saveLlmConnection(input, { storage = "auto" } = {}) {
|
|
|
98
101
|
baseUrl: value.baseUrl,
|
|
99
102
|
visionModel: value.visionModel || null,
|
|
100
103
|
};
|
|
104
|
+
// A keyless custom endpoint must never receive the previous target's key.
|
|
105
|
+
if (!sameEndpoint && !value.apiKey && value.provider === "custom") {
|
|
106
|
+
delete config.llm.apiKey;
|
|
107
|
+
delete config.llm.apiKeyHelper;
|
|
108
|
+
}
|
|
101
109
|
};
|
|
102
110
|
// A single lock and atomic rename bind endpoint and credential together.
|
|
103
111
|
// An interrupted save cannot route the old credential to the new endpoint.
|
|
@@ -3,6 +3,7 @@ import { estimateCost } from "./llm-pricing.js";
|
|
|
3
3
|
import { providerFetch } from "./formal-budget-provider-scope.js";
|
|
4
4
|
import { isFormalBudgetError } from "./formal-budget-errors.js";
|
|
5
5
|
import { normalizeOllamaBaseUrl } from "./ollama-base-url.js";
|
|
6
|
+
import { assertCustomProviderConnection } from "./provider-protocol.js";
|
|
6
7
|
import { resolveModelCapabilityProfile } from "./model-capabilities.js";
|
|
7
8
|
import {
|
|
8
9
|
projectEnvelopePricingContext,
|
|
@@ -159,6 +160,7 @@ export async function probeLlmConnection(
|
|
|
159
160
|
target,
|
|
160
161
|
{ fetchImpl = fetch, timeoutMs = 20_000, includeUsage = false } = {},
|
|
161
162
|
) {
|
|
163
|
+
assertCustomProviderConnection(target.provider, target);
|
|
162
164
|
const base =
|
|
163
165
|
target.provider === "ollama"
|
|
164
166
|
? normalizeOllamaBaseUrl(target.baseUrl)
|
|
@@ -183,7 +185,8 @@ export async function probeLlmConnection(
|
|
|
183
185
|
options: { num_predict: PROBE_OUTPUT_TOKENS },
|
|
184
186
|
};
|
|
185
187
|
} else {
|
|
186
|
-
if (!target.apiKey
|
|
188
|
+
if (!target.apiKey && target.provider !== "custom")
|
|
189
|
+
throw new Error("API key required");
|
|
187
190
|
if (target.provider === "anthropic") {
|
|
188
191
|
url = `${base}/messages`;
|
|
189
192
|
headers["x-api-key"] = target.apiKey;
|
|
@@ -211,7 +214,7 @@ export async function probeLlmConnection(
|
|
|
211
214
|
});
|
|
212
215
|
} else {
|
|
213
216
|
url = `${base}/chat/completions`;
|
|
214
|
-
headers.Authorization = `Bearer ${target.apiKey}`;
|
|
217
|
+
if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
|
|
215
218
|
body = {
|
|
216
219
|
model: target.model,
|
|
217
220
|
messages: [{ role: "user", content: "Say hi in one word." }],
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { BUILT_IN_PROVIDERS } from "./llm-providers.js";
|
|
2
2
|
import { normalizeOllamaBaseUrl } from "./ollama-base-url.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
assertCustomProviderConnection,
|
|
5
|
+
normalizeProviderBaseUrl,
|
|
6
|
+
} from "./provider-protocol.js";
|
|
4
7
|
import { resolveApiKeyFromHelper } from "./api-key-helper.js";
|
|
5
8
|
|
|
6
9
|
const DEFAULT_PROVIDER = "ollama";
|
|
@@ -33,9 +36,8 @@ export function resolveLlmConnection(
|
|
|
33
36
|
defaultBaseUrl = null,
|
|
34
37
|
} = {},
|
|
35
38
|
) {
|
|
36
|
-
const sources = (
|
|
37
|
-
? configuredSources
|
|
38
|
-
: [configuredSources]
|
|
39
|
+
const sources = (
|
|
40
|
+
Array.isArray(configuredSources) ? configuredSources : [configuredSources]
|
|
39
41
|
).filter((source) => source && typeof source === "object");
|
|
40
42
|
const provider =
|
|
41
43
|
(nonEmpty(options.provider) && options.provider) ||
|
|
@@ -67,6 +69,7 @@ export function resolveLlmConnection(
|
|
|
67
69
|
defaultModel ||
|
|
68
70
|
definition?.models?.[0] ||
|
|
69
71
|
null;
|
|
72
|
+
assertCustomProviderConnection(provider, { baseUrl, model });
|
|
70
73
|
let apiKey =
|
|
71
74
|
(nonEmpty(options.apiKey) && options.apiKey) ||
|
|
72
75
|
(nonEmpty(env?.CC_API_KEY) && env.CC_API_KEY) ||
|
package/src/lib/llm-providers.js
CHANGED
|
@@ -127,6 +127,19 @@ export const BUILT_IN_PROVIDERS = {
|
|
|
127
127
|
},
|
|
128
128
|
};
|
|
129
129
|
|
|
130
|
+
/** Custom platforms use Chat Completions with an explicit endpoint and model. */
|
|
131
|
+
export function resolveProviderDefinition(provider) {
|
|
132
|
+
return provider === "custom"
|
|
133
|
+
? {
|
|
134
|
+
name: "custom",
|
|
135
|
+
baseUrl: null,
|
|
136
|
+
models: [],
|
|
137
|
+
apiKeyEnv: "CC_API_KEY",
|
|
138
|
+
custom: true,
|
|
139
|
+
}
|
|
140
|
+
: BUILT_IN_PROVIDERS[provider];
|
|
141
|
+
}
|
|
142
|
+
|
|
130
143
|
// A connectivity probe (`testProvider`) sends a tiny "Hi" and expects a quick
|
|
131
144
|
// reply, so it must fail fast — a probe that hangs forever defeats its purpose
|
|
132
145
|
// (it backs `cc llm test`, provider selection, doctor). Bound it with a total
|
|
@@ -402,17 +415,17 @@ export class LLMProviderRegistry {
|
|
|
402
415
|
const res = await _fetchWithTimeout(
|
|
403
416
|
`${normalizeProviderBaseUrl(provider.baseUrl)}/messages`,
|
|
404
417
|
{
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
418
|
+
method: "POST",
|
|
419
|
+
headers: {
|
|
420
|
+
"Content-Type": "application/json",
|
|
421
|
+
"x-api-key": key,
|
|
422
|
+
"anthropic-version": "2023-06-01",
|
|
423
|
+
},
|
|
424
|
+
body: JSON.stringify({
|
|
425
|
+
model: testModel,
|
|
426
|
+
max_tokens: 10,
|
|
427
|
+
messages: [{ role: "user", content: "Hi" }],
|
|
428
|
+
}),
|
|
416
429
|
},
|
|
417
430
|
);
|
|
418
431
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
@@ -103,7 +103,8 @@ function sanitizeReasoningItems(items) {
|
|
|
103
103
|
type: "reasoning",
|
|
104
104
|
...(id ? { id } : {}),
|
|
105
105
|
...(encryptedContent ? { encrypted_content: encryptedContent } : {}),
|
|
106
|
-
|
|
106
|
+
// Responses requires this field even when no summary was requested.
|
|
107
|
+
summary,
|
|
107
108
|
...(status ? { status } : {}),
|
|
108
109
|
});
|
|
109
110
|
}
|