chainlesschain 0.166.132 → 0.166.135
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 +44 -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/background-launch-profile.js +37 -1
- package/src/lib/chat-core.js +40 -35
- package/src/lib/coding-task-guidance.js +10 -9
- package/src/lib/context-memory-kernel/message-adapter.js +5 -1
- 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 +182 -34
- package/src/runtime/headless-runner.js +3 -3
package/src/lib/chat-core.js
CHANGED
|
@@ -10,9 +10,13 @@ import { providerFetch } from "./formal-budget-provider-scope.js";
|
|
|
10
10
|
* - streamOllama / streamOpenAI — low-level streaming helpers
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
BUILT_IN_PROVIDERS,
|
|
15
|
+
resolveProviderDefinition,
|
|
16
|
+
} from "./llm-providers.js";
|
|
14
17
|
import { normalizeOllamaBaseUrl } from "./ollama-base-url.js";
|
|
15
18
|
import {
|
|
19
|
+
assertCustomProviderConnection,
|
|
16
20
|
geminiGenerateContentUrl,
|
|
17
21
|
geminiRequestBody,
|
|
18
22
|
geminiUsage,
|
|
@@ -262,22 +266,22 @@ export async function streamOpenAI(
|
|
|
262
266
|
response = await providerFetch(
|
|
263
267
|
`${normalizeProviderBaseUrl(baseUrl)}/chat/completions`,
|
|
264
268
|
{
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
269
|
+
method: "POST",
|
|
270
|
+
headers: {
|
|
271
|
+
"Content-Type": "application/json",
|
|
272
|
+
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
273
|
+
},
|
|
274
|
+
body: JSON.stringify({
|
|
275
|
+
model,
|
|
276
|
+
messages,
|
|
277
|
+
stream: true,
|
|
278
|
+
// Opt-in token usage in the terminal chunk (OpenAI-compatible).
|
|
279
|
+
// Servers that don't understand it simply ignore it.
|
|
280
|
+
stream_options: { include_usage: true },
|
|
281
|
+
}),
|
|
282
|
+
signal: options.signal
|
|
283
|
+
? AbortSignal.any([guard.signal, options.signal])
|
|
284
|
+
: guard.signal,
|
|
281
285
|
},
|
|
282
286
|
);
|
|
283
287
|
} catch (e) {
|
|
@@ -410,22 +414,22 @@ export async function streamAnthropic(
|
|
|
410
414
|
response = await providerFetch(
|
|
411
415
|
`${normalizeProviderBaseUrl(baseUrl)}/messages`,
|
|
412
416
|
{
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
417
|
+
method: "POST",
|
|
418
|
+
headers: {
|
|
419
|
+
"Content-Type": "application/json",
|
|
420
|
+
"x-api-key": apiKey,
|
|
421
|
+
"anthropic-version": "2023-06-01",
|
|
422
|
+
},
|
|
423
|
+
body: JSON.stringify({
|
|
424
|
+
model,
|
|
425
|
+
max_tokens: 4096,
|
|
426
|
+
stream: true,
|
|
427
|
+
...(system ? { system } : {}),
|
|
428
|
+
messages: convo,
|
|
429
|
+
}),
|
|
430
|
+
signal: options.signal
|
|
431
|
+
? AbortSignal.any([guard.signal, options.signal])
|
|
432
|
+
: guard.signal,
|
|
429
433
|
},
|
|
430
434
|
);
|
|
431
435
|
} catch (e) {
|
|
@@ -666,6 +670,7 @@ export async function streamGemini(
|
|
|
666
670
|
* @param {object} options - provider, model, baseUrl, apiKey
|
|
667
671
|
*/
|
|
668
672
|
export async function* chatStream(messages, options) {
|
|
673
|
+
assertCustomProviderConnection(options.provider, options);
|
|
669
674
|
const { provider, model, baseUrl, apiKey, sessionId } = options;
|
|
670
675
|
|
|
671
676
|
// Token queue + waiter promise. onToken (called synchronously by each
|
|
@@ -765,14 +770,14 @@ export async function* chatStream(messages, options) {
|
|
|
765
770
|
options,
|
|
766
771
|
);
|
|
767
772
|
} else {
|
|
768
|
-
const providerDef =
|
|
773
|
+
const providerDef = resolveProviderDefinition(provider);
|
|
769
774
|
const url = normalizeProviderBaseUrl(
|
|
770
775
|
baseUrl || providerDef?.baseUrl || "https://api.openai.com/v1",
|
|
771
776
|
);
|
|
772
777
|
const key =
|
|
773
778
|
apiKey ||
|
|
774
779
|
(providerDef?.apiKeyEnv ? process.env[providerDef.apiKeyEnv] : null);
|
|
775
|
-
if (!key) {
|
|
780
|
+
if (!key && provider !== "custom") {
|
|
776
781
|
throw new Error(
|
|
777
782
|
`API key required for ${provider} (set ${providerDef?.apiKeyEnv || "API key"})`,
|
|
778
783
|
);
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/** Shared correctness guidance for normal and file-only coding sessions. */
|
|
2
2
|
export const CODING_TASK_QUALITY_GUIDANCE = `Coding task quality:
|
|
3
|
-
- Read the actual implementation and its exported API before writing a caller or test.
|
|
4
|
-
- Keep progress explanations to one or two sentences.
|
|
5
|
-
- Resolve each relative import from the destination file's directory. Read back
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
- For
|
|
9
|
-
- Trace rejection before asserting its aftermath:
|
|
10
|
-
-
|
|
11
|
-
-
|
|
3
|
+
- Read the actual implementation and its exported API before writing a caller or test. Verify parameter order, return shapes, fixture requirements and the delegated owner of each lifecycle transition.
|
|
4
|
+
- Keep progress explanations to one or two sentences. Resolve uncertainty with targeted reads, reuse unchanged source already inspected, and put code directly in write/edit arguments.
|
|
5
|
+
- Resolve each relative import from the destination file's directory. Read back completed files and fix newDiagnostics. A successful write proves persistence, not test execution or acceptance.
|
|
6
|
+
- Map every acceptance requirement to a fixture, real production call path and observable assertion. Positive and negative controls must reach the same boundary with unrelated prerequisites valid. Avoid mocks that bypass it; identify which assertion would fail without that boundary.
|
|
7
|
+
- Derive case allocation from the user's requirements. A test-count cap does not waive coverage: merge redundant cases to cover gaps within the cap, authorized scope and remaining budget. Report unresolved gaps as incomplete.
|
|
8
|
+
- For races, control the intended transition and complete the current worker/request when testing cancellation or replacement. An earlier unrelated rejection does not prove the target guard. Derive expectations independently from the contract or authoritative state, not by calling the function under test.
|
|
9
|
+
- Trace rejection before asserting its aftermath: it may precede registration or persistence. Inspect whether state stays unchanged, a failed record exists or an event is emitted. Compare independently recorded identities and provenance; equal text does not prove ownership.
|
|
10
|
+
- Inspect persistence projections before comparing histories. Host notices, lineage and imports may accompany user/assistant messages; verify their provenance separately. Report defects that cannot be repaired within the permitted scope instead of inventing passing behavior.
|
|
11
|
+
- Run focused checks with permitted execution tools. With file tools only, inspect imports, contracts and fixtures and say tests were not run. Read-back or static inspection is not a passing test.
|
|
12
|
+
- Finish requested edits and read-back before the final reply; reserve the last available model request for a concise terminal account. For large files, write a small complete section, await its result, then extend it with a unique edit anchor rather than rewriting the growing file. Report delivery, actual checks and limitations once.`;
|
|
12
13
|
|
|
13
14
|
export const HERMETIC_CODING_SYSTEM_PROMPT = `You are a hermetic coding assistant. The exposed file tools are the complete tool surface. Inspect and mutate only the exact paths authorized by the user request. Do not invoke hidden tools, commands, language servers or other execution paths.
|
|
14
15
|
|
|
@@ -549,7 +549,11 @@ export function createSummaryContextItem({
|
|
|
549
549
|
Math.ceil((Buffer.byteLength(content, "utf8") - image.bytes) / 4) +
|
|
550
550
|
image.tokens,
|
|
551
551
|
),
|
|
552
|
-
|
|
552
|
+
// Keep the new handoff ahead of ordinary conversation in the final plan.
|
|
553
|
+
// Otherwise newer messages can fill the partition and evict the summary
|
|
554
|
+
// of the already-dropped history. This is selection priority only: the
|
|
555
|
+
// summary stays unpinned, data-only and subject to the same token ceilings.
|
|
556
|
+
priority: Math.max(600_000, ...parents.map((item) => item.priority)),
|
|
553
557
|
pinned: false,
|
|
554
558
|
createdAt: new Date(now).toISOString(),
|
|
555
559
|
...contentFields,
|
|
@@ -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
|
}
|