@cjhyy/code-shell-core 0.9.2 → 0.9.4
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/cli/agent-server-stdio.js +26 -1
- package/dist/cli/agent-server-tcp.js +58 -7
- package/dist/engine/engine.d.ts +8 -0
- package/dist/engine/engine.js +16 -12
- package/dist/engine/model-facade.d.ts +3 -0
- package/dist/engine/model-facade.js +2 -0
- package/dist/engine/run-tooling.js +8 -8
- package/dist/engine/streaming-tool-queue.d.ts +4 -1
- package/dist/engine/streaming-tool-queue.js +17 -2
- package/dist/engine/turn-loop.d.ts +14 -5
- package/dist/engine/turn-loop.js +92 -37
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/llm/prompt-cache.d.ts +48 -0
- package/dist/llm/prompt-cache.js +100 -0
- package/dist/llm/providers/anthropic.d.ts +3 -0
- package/dist/llm/providers/anthropic.js +77 -53
- package/dist/llm/providers/openai.d.ts +7 -27
- package/dist/llm/providers/openai.js +120 -68
- package/dist/llm/types.d.ts +3 -0
- package/dist/onboarding.js +72 -49
- package/dist/panel-apps/manifest.d.ts +12 -12
- package/dist/profile/types.d.ts +26 -26
- package/dist/protocol/background-result-wakeup.d.ts +3 -1
- package/dist/protocol/background-result-wakeup.js +5 -5
- package/dist/protocol/server.d.ts +13 -0
- package/dist/protocol/server.js +22 -3
- package/dist/services/index.d.ts +0 -1
- package/dist/services/index.js +0 -1
- package/dist/session/memory.js +2 -2
- package/dist/session/session-manager.js +30 -1
- package/dist/tool-system/builtin/agent-notifications.d.ts +25 -1
- package/dist/tool-system/builtin/agent-notifications.js +334 -2
- package/dist/tool-system/context.d.ts +9 -4
- package/dist/tool-system/external-tool-exposure.js +11 -10
- package/package.json +2 -1
- package/dist/services/notifier.d.ts +0 -33
- package/dist/services/notifier.js +0 -83
|
@@ -16,7 +16,8 @@ import { capabilitiesFor } from "../capabilities/index.js";
|
|
|
16
16
|
import { clampMaxTokens } from "../clamp-max-tokens.js";
|
|
17
17
|
import { resolveApiKey, resolveHeaders } from "../provider-auth.js";
|
|
18
18
|
import { stripVisionFromHistory } from "../strip-vision.js";
|
|
19
|
-
import {
|
|
19
|
+
import { resolvePromptCachePolicy, uniquePromptCacheBreakpointIndexes, } from "../prompt-cache.js";
|
|
20
|
+
import { STREAM_WATCHDOG_CONFIG, StreamIdleTimeoutError } from "../stream-watchdog.js";
|
|
20
21
|
/**
|
|
21
22
|
* Extract prompt-cache counts from an OpenAI-compatible usage object.
|
|
22
23
|
*
|
|
@@ -74,9 +75,7 @@ export async function runStreamWithWatchdog(stream, opts = {}) {
|
|
|
74
75
|
// An explicit idleTimeoutMs always activates the watchdog. Otherwise follow
|
|
75
76
|
// disableWatchdog (per-call override) if set, else the env default.
|
|
76
77
|
const watchdogActive = opts.idleTimeoutMs !== undefined ||
|
|
77
|
-
(opts.disableWatchdog === undefined
|
|
78
|
-
? STREAM_WATCHDOG_CONFIG.enabled
|
|
79
|
-
: !opts.disableWatchdog);
|
|
78
|
+
(opts.disableWatchdog === undefined ? STREAM_WATCHDOG_CONFIG.enabled : !opts.disableWatchdog);
|
|
80
79
|
const idleTimeoutMs = opts.idleTimeoutMs ?? STREAM_WATCHDOG_CONFIG.idleTimeoutMs;
|
|
81
80
|
let text = "";
|
|
82
81
|
// Fast path: watchdog disabled AND caller did not override → no overhead.
|
|
@@ -224,6 +223,9 @@ export class OpenAIClient extends LLMClientBase {
|
|
|
224
223
|
// succeed. Omitting the field just means "model default reasoning", which is
|
|
225
224
|
// fine for our background/aux calls.
|
|
226
225
|
_dropReasoningEffort = false;
|
|
226
|
+
/** Compatibility fallbacks for OpenAI-compatible gateways that lag the API. */
|
|
227
|
+
_disableExplicitPromptCache = false;
|
|
228
|
+
_disablePromptCacheKey = false;
|
|
227
229
|
constructor(config, defaults, runtimeOptions = {}) {
|
|
228
230
|
super(config, defaults);
|
|
229
231
|
this.dangerouslyAllowBrowser = runtimeOptions.dangerouslyAllowBrowser === true;
|
|
@@ -264,30 +266,26 @@ export class OpenAIClient extends LLMClientBase {
|
|
|
264
266
|
}
|
|
265
267
|
return this._capability;
|
|
266
268
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
* "anthropic/".
|
|
279
|
-
*/
|
|
280
|
-
get isOpenRouterAnthropic() {
|
|
281
|
-
return this.config.providerKind === "openrouter" && /^~?anthropic\//.test(this.model);
|
|
269
|
+
promptCachePolicy(request) {
|
|
270
|
+
const policy = resolvePromptCachePolicy({
|
|
271
|
+
provider: this.provider,
|
|
272
|
+
providerKind: this.config.providerKind,
|
|
273
|
+
model: this.model,
|
|
274
|
+
request,
|
|
275
|
+
explicitDisabled: this._disableExplicitPromptCache,
|
|
276
|
+
});
|
|
277
|
+
return this._disablePromptCacheKey && policy.cacheKey
|
|
278
|
+
? { ...policy, cacheKey: undefined }
|
|
279
|
+
: policy;
|
|
282
280
|
}
|
|
283
281
|
getPromptCacheConfigIdentity() {
|
|
284
282
|
const capability = this.capability;
|
|
283
|
+
const cachePolicy = this.promptCachePolicy();
|
|
285
284
|
return {
|
|
286
285
|
...super.getPromptCacheConfigIdentity(),
|
|
287
|
-
cacheStrategy:
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
cacheLayoutVersion: this.isOpenRouterAnthropic ? "system-history-v1" : "automatic-v1",
|
|
286
|
+
cacheStrategy: cachePolicy.strategy,
|
|
287
|
+
cacheLayoutVersion: cachePolicy.layoutVersion,
|
|
288
|
+
cacheBreakpoints: cachePolicy.breakpoints,
|
|
291
289
|
tokenLimitField: this._forceMaxCompletionTokens
|
|
292
290
|
? "max_completion_tokens"
|
|
293
291
|
: capability.tokenLimitField,
|
|
@@ -295,6 +293,8 @@ export class OpenAIClient extends LLMClientBase {
|
|
|
295
293
|
rejectedParams: [...capability.rejectedParams].sort(),
|
|
296
294
|
forceMaxCompletionTokens: this._forceMaxCompletionTokens,
|
|
297
295
|
dropReasoningEffort: this._dropReasoningEffort,
|
|
296
|
+
disableExplicitPromptCache: this._disableExplicitPromptCache,
|
|
297
|
+
disablePromptCacheKey: this._disablePromptCacheKey,
|
|
298
298
|
};
|
|
299
299
|
}
|
|
300
300
|
async createMessage(options) {
|
|
@@ -305,7 +305,7 @@ export class OpenAIClient extends LLMClientBase {
|
|
|
305
305
|
// Per-call reasoning wins; otherwise fall back to provider default
|
|
306
306
|
// (settings.providers[].reasoning, threaded through LLMConfig).
|
|
307
307
|
const reasoning = options.reasoning ?? this.config.reasoning;
|
|
308
|
-
const messages = this.buildMessages(options.systemPrompt, options.messages, reasoning);
|
|
308
|
+
const messages = this.buildMessages(options.systemPrompt, options.messages, reasoning, options.promptCache);
|
|
309
309
|
const tools = options.tools?.length ? this.convertTools(options.tools) : undefined;
|
|
310
310
|
const span = logger.span("llm.request", {
|
|
311
311
|
cat: "llm",
|
|
@@ -314,6 +314,7 @@ export class OpenAIClient extends LLMClientBase {
|
|
|
314
314
|
stream: !!(options.stream && options.onChunk),
|
|
315
315
|
messageCount: messages.length,
|
|
316
316
|
toolCount: tools?.length ?? 0,
|
|
317
|
+
cacheStrategy: this.promptCachePolicy(options.promptCache).strategy,
|
|
317
318
|
});
|
|
318
319
|
try {
|
|
319
320
|
const response = options.stream && options.onChunk
|
|
@@ -341,6 +342,7 @@ export class OpenAIClient extends LLMClientBase {
|
|
|
341
342
|
*/
|
|
342
343
|
buildRequestBody(options, messages, tools, reasoning, stream) {
|
|
343
344
|
const cap = this.capability;
|
|
345
|
+
const cachePolicy = this.promptCachePolicy(options.promptCache);
|
|
344
346
|
// Clamp to the model's known output ceiling so a stale catalog value
|
|
345
347
|
// (e.g. 384000 inherited after a hot model switch) can't 400 a
|
|
346
348
|
// smaller-cap model. No known cap → send the value as-is.
|
|
@@ -449,10 +451,10 @@ export class OpenAIClient extends LLMClientBase {
|
|
|
449
451
|
// requested summary level to that object. For the bare `reasoning_effort`
|
|
450
452
|
// shape there's no summary field on chat-completions, so we skip it rather
|
|
451
453
|
// than send an unknown top-level param.
|
|
452
|
-
if (this.config.reasoningSummary &&
|
|
454
|
+
if (this.config.reasoningSummary &&
|
|
455
|
+
reasoningBody.reasoning &&
|
|
453
456
|
typeof reasoningBody.reasoning === "object") {
|
|
454
|
-
reasoningBody.reasoning.summary =
|
|
455
|
-
this.config.reasoningSummary;
|
|
457
|
+
reasoningBody.reasoning.summary = this.config.reasoningSummary;
|
|
456
458
|
}
|
|
457
459
|
// Catalog-driven passthrough params (temperature/top_p/thinking etc, already
|
|
458
460
|
// wire-mapped from the connection's paramValues by applyParams). Filter each
|
|
@@ -489,6 +491,12 @@ export class OpenAIClient extends LLMClientBase {
|
|
|
489
491
|
...paramBody,
|
|
490
492
|
// service_tier (TODO 7.2): passed through verbatim when configured.
|
|
491
493
|
...(this.config.serviceTier ? { service_tier: this.config.serviceTier } : {}),
|
|
494
|
+
...(options.promptCache && cachePolicy.cacheKey
|
|
495
|
+
? { prompt_cache_key: cachePolicy.cacheKey }
|
|
496
|
+
: {}),
|
|
497
|
+
...(options.promptCache && cachePolicy.promptCacheOptions
|
|
498
|
+
? { prompt_cache_options: cachePolicy.promptCacheOptions }
|
|
499
|
+
: {}),
|
|
492
500
|
...(tools ? { tools } : {}),
|
|
493
501
|
...(stream ? { stream: true, stream_options: { include_usage: true } } : {}),
|
|
494
502
|
};
|
|
@@ -740,7 +748,7 @@ export class OpenAIClient extends LLMClientBase {
|
|
|
740
748
|
...(reasoningContent ? { reasoningContent } : {}),
|
|
741
749
|
};
|
|
742
750
|
}
|
|
743
|
-
buildMessages(systemPrompt, messages, reasoning) {
|
|
751
|
+
buildMessages(systemPrompt, messages, reasoning, promptCache) {
|
|
744
752
|
const result = [{ role: "system", content: systemPrompt }];
|
|
745
753
|
// Drop historical image blocks when the active model can't accept vision.
|
|
746
754
|
// Engine.run only gates *new* attachments; an image left in history from
|
|
@@ -748,6 +756,8 @@ export class OpenAIClient extends LLMClientBase {
|
|
|
748
756
|
// below and 400s ("unknown variant `image_url`") after a model switch.
|
|
749
757
|
// Identity-preserving on the common path (vision models / no images).
|
|
750
758
|
messages = stripVisionFromHistory(messages, this.capability.supportsVision);
|
|
759
|
+
const stablePrefixMessageCount = Math.max(0, Math.min(messages.length, promptCache?.stablePrefixMessageCount ?? messages.length));
|
|
760
|
+
let stablePrefixEndMessage = stablePrefixMessageCount === 0 ? result[0] : undefined;
|
|
751
761
|
// Reasoning-content echo-back contract — driven by capability:
|
|
752
762
|
// "when-tools" : backfill an empty placeholder if the prior assistant
|
|
753
763
|
// turn doesn't carry one (DeepSeek V4 + tools 400s
|
|
@@ -760,11 +770,13 @@ export class OpenAIClient extends LLMClientBase {
|
|
|
760
770
|
const cap = this.capability;
|
|
761
771
|
const hasTools = messages.some((m) => Array.isArray(m.content) &&
|
|
762
772
|
m.content.some((b) => b.type === "tool_use" || b.type === "tool_result"));
|
|
763
|
-
const needsReasoningBackfill = reasoning?.mode !== "off" &&
|
|
764
|
-
cap.echoReasoning === "when-tools" &&
|
|
765
|
-
hasTools;
|
|
773
|
+
const needsReasoningBackfill = reasoning?.mode !== "off" && cap.echoReasoning === "when-tools" && hasTools;
|
|
766
774
|
const stripReasoning = cap.echoReasoning === "never";
|
|
767
|
-
for (
|
|
775
|
+
for (let sourceIndex = 0; sourceIndex < messages.length; sourceIndex++) {
|
|
776
|
+
if (sourceIndex === stablePrefixMessageCount) {
|
|
777
|
+
stablePrefixEndMessage = result[result.length - 1];
|
|
778
|
+
}
|
|
779
|
+
const msg = messages[sourceIndex];
|
|
768
780
|
if (msg.role === "system")
|
|
769
781
|
continue;
|
|
770
782
|
if (msg.role === "assistant") {
|
|
@@ -948,55 +960,64 @@ export class OpenAIClient extends LLMClientBase {
|
|
|
948
960
|
}
|
|
949
961
|
}
|
|
950
962
|
}
|
|
963
|
+
stablePrefixEndMessage ??= result[result.length - 1];
|
|
951
964
|
const normalized = normalizeOpenAIToolMessagePairs(result);
|
|
952
|
-
|
|
953
|
-
|
|
965
|
+
const stablePrefixEndIndex = stablePrefixEndMessage
|
|
966
|
+
? normalized.indexOf(stablePrefixEndMessage)
|
|
967
|
+
: undefined;
|
|
968
|
+
const cachePolicy = this.promptCachePolicy(promptCache);
|
|
969
|
+
if (promptCache || cachePolicy.strategy === "anthropic-explicit") {
|
|
970
|
+
this.applyPromptCacheBreakpoints(normalized, cachePolicy, stablePrefixEndIndex !== undefined && stablePrefixEndIndex >= 0
|
|
971
|
+
? stablePrefixEndIndex
|
|
972
|
+
: undefined);
|
|
954
973
|
}
|
|
955
974
|
return normalized;
|
|
956
975
|
}
|
|
957
976
|
/**
|
|
958
|
-
*
|
|
959
|
-
*
|
|
960
|
-
* 1. System block — the stable prefix. Anthropic sees tools BEFORE the
|
|
961
|
-
* system prompt, so one marker on the system block caches tools too
|
|
962
|
-
* (verified live: system-only marker cached 3511/3952 prompt tokens
|
|
963
|
-
* including tool defs).
|
|
964
|
-
* 2. Last message — one rolling breakpoint so the growing conversation
|
|
965
|
-
* history becomes a cached prefix. Not scrolled: as history grows the
|
|
966
|
-
* "last message" naturally advances and its tail is the next write.
|
|
967
|
-
* A string `content` is lifted to a single-element `[{type:"text",...}]`
|
|
968
|
-
* array so it can carry `cache_control`; OpenRouter accepts this OpenAI
|
|
969
|
-
* multimodal wire form for text.
|
|
977
|
+
* Translate semantic prefix boundaries to the active wire format. Both
|
|
978
|
+
* formats annotate content blocks without reordering messages.
|
|
970
979
|
*/
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
980
|
+
applyPromptCacheBreakpoints(messages, policy, stablePrefixEndIndex) {
|
|
981
|
+
if (policy.strategy !== "anthropic-explicit" && policy.strategy !== "openai-explicit") {
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
const markedMessages = new Set();
|
|
985
|
+
const mark = (index) => {
|
|
986
|
+
let cursor = Math.min(index, messages.length - 1);
|
|
987
|
+
let m;
|
|
988
|
+
while (cursor >= 0) {
|
|
989
|
+
const candidate = messages[cursor];
|
|
990
|
+
if ((typeof candidate.content === "string" && candidate.content.length > 0) ||
|
|
991
|
+
(Array.isArray(candidate.content) && candidate.content.length > 0)) {
|
|
992
|
+
m = candidate;
|
|
993
|
+
break;
|
|
994
|
+
}
|
|
995
|
+
cursor--;
|
|
996
|
+
}
|
|
997
|
+
if (!m || markedMessages.has(m))
|
|
974
998
|
return;
|
|
999
|
+
markedMessages.add(m);
|
|
1000
|
+
const marker = policy.strategy === "anthropic-explicit"
|
|
1001
|
+
? { cache_control: { type: "ephemeral" } }
|
|
1002
|
+
: { prompt_cache_breakpoint: { mode: "explicit" } };
|
|
975
1003
|
// Lift a plain-string content to a text-block array so it can carry the
|
|
976
|
-
// marker. Non-text content
|
|
977
|
-
// array of parts — mark the last part instead.
|
|
1004
|
+
// marker. Non-text content already uses an array of parts.
|
|
978
1005
|
if (typeof m.content === "string") {
|
|
979
|
-
m.content = [
|
|
980
|
-
{ type: "text", text: m.content, cache_control: { type: "ephemeral" } },
|
|
981
|
-
];
|
|
1006
|
+
m.content = [{ type: "text", text: m.content, ...marker }];
|
|
982
1007
|
return;
|
|
983
1008
|
}
|
|
984
1009
|
if (Array.isArray(m.content) && m.content.length > 0) {
|
|
985
|
-
// cache_control is an Anthropic-via-OpenRouter extension field, not in
|
|
986
|
-
// the OpenAI content-part union — attach through `unknown`.
|
|
987
1010
|
const last = m.content[m.content.length - 1];
|
|
988
|
-
last
|
|
1011
|
+
Object.assign(last, marker);
|
|
989
1012
|
}
|
|
990
1013
|
};
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
if (last && last !== sys)
|
|
999
|
-
mark(last);
|
|
1014
|
+
const requested = uniquePromptCacheBreakpointIndexes([
|
|
1015
|
+
policy.breakpoints.includes("system") ? 0 : undefined,
|
|
1016
|
+
policy.breakpoints.includes("stable-history") ? stablePrefixEndIndex : undefined,
|
|
1017
|
+
policy.breakpoints.includes("rolling-history") ? messages.length - 1 : undefined,
|
|
1018
|
+
]);
|
|
1019
|
+
for (const index of requested)
|
|
1020
|
+
mark(index);
|
|
1000
1021
|
}
|
|
1001
1022
|
convertTools(tools) {
|
|
1002
1023
|
return tools.map((t) => ({
|
|
@@ -1031,6 +1052,33 @@ export class OpenAIClient extends LLMClientBase {
|
|
|
1031
1052
|
// the corrected body — fixing the call that triggered it, not just the
|
|
1032
1053
|
// next one.
|
|
1033
1054
|
let selfCorrected = false;
|
|
1055
|
+
// New GPT-5.6 cache fields may reach an OpenAI-compatible gateway before
|
|
1056
|
+
// that gateway supports them. Downgrade sticky-per-client and retry once:
|
|
1057
|
+
// first explicit -> implicit, then omit the affinity key only if that is
|
|
1058
|
+
// also rejected. Native OpenAI keeps the optimized path.
|
|
1059
|
+
if (err.status === 400 && msg.includes("prompt_cache")) {
|
|
1060
|
+
if ((msg.includes("prompt_cache_options") || msg.includes("prompt_cache_breakpoint")) &&
|
|
1061
|
+
!this._disableExplicitPromptCache) {
|
|
1062
|
+
this._disableExplicitPromptCache = true;
|
|
1063
|
+
selfCorrected = true;
|
|
1064
|
+
logger.warn("llm.prompt_cache_explicit_unsupported", {
|
|
1065
|
+
cat: "llm",
|
|
1066
|
+
provider: this.provider,
|
|
1067
|
+
providerKind: this.config.providerKind,
|
|
1068
|
+
model: this.model,
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
if (msg.includes("prompt_cache_key") && !this._disablePromptCacheKey) {
|
|
1072
|
+
this._disablePromptCacheKey = true;
|
|
1073
|
+
selfCorrected = true;
|
|
1074
|
+
logger.warn("llm.prompt_cache_key_unsupported", {
|
|
1075
|
+
cat: "llm",
|
|
1076
|
+
provider: this.provider,
|
|
1077
|
+
providerKind: this.config.providerKind,
|
|
1078
|
+
model: this.model,
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1034
1082
|
// o-series / gpt-5+ reject `max_tokens` and demand
|
|
1035
1083
|
// `max_completion_tokens`. The id-based regex catches the common
|
|
1036
1084
|
// cases; this is the belt-and-suspenders path for ids that ship
|
|
@@ -1095,8 +1143,12 @@ export class OpenAIClient extends LLMClientBase {
|
|
|
1095
1143
|
function deepMergeInto(dst, src) {
|
|
1096
1144
|
for (const [k, v] of Object.entries(src)) {
|
|
1097
1145
|
const cur = dst[k];
|
|
1098
|
-
if (v &&
|
|
1099
|
-
|
|
1146
|
+
if (v &&
|
|
1147
|
+
typeof v === "object" &&
|
|
1148
|
+
!Array.isArray(v) &&
|
|
1149
|
+
cur &&
|
|
1150
|
+
typeof cur === "object" &&
|
|
1151
|
+
!Array.isArray(cur)) {
|
|
1100
1152
|
deepMergeInto(cur, v);
|
|
1101
1153
|
}
|
|
1102
1154
|
else {
|
package/dist/llm/types.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* LLM-specific internal types.
|
|
3
3
|
*/
|
|
4
4
|
import type { Message, ToolDefinition, LLMStreamChunk, TokenUsage } from "../types.js";
|
|
5
|
+
import type { PromptCacheRequestContext } from "./prompt-cache.js";
|
|
5
6
|
export interface CreateMessageOptions {
|
|
6
7
|
systemPrompt: string;
|
|
7
8
|
messages: Message[];
|
|
@@ -33,6 +34,8 @@ export interface CreateMessageOptions {
|
|
|
33
34
|
* the client's capability layer.
|
|
34
35
|
*/
|
|
35
36
|
reasoning?: import("./reasoning-setting.js").ReasoningSetting;
|
|
37
|
+
/** Provider-neutral prompt-cache context supplied by the model facade. */
|
|
38
|
+
promptCache?: PromptCacheRequestContext;
|
|
36
39
|
}
|
|
37
40
|
export interface LLMUsageTracker {
|
|
38
41
|
records: TokenUsage[];
|
package/dist/onboarding.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* (see src/ui/components/OnboardingPrompt.tsx); this module only exposes
|
|
5
5
|
* pure(-ish) helpers it consumes, so there's a single input stack.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { userHome } from "./settings/manager.js";
|
|
10
10
|
import { getOpenRouterModels } from "./data/openrouter-models.js";
|
|
@@ -14,6 +14,7 @@ import { KNOWN_MAX_OUTPUT, KNOWN_CONTEXT_WINDOWS, OPENROUTER_VENDORS, PROVIDERS,
|
|
|
14
14
|
export { PROVIDERS };
|
|
15
15
|
import { sanitizeApiKey } from "./llm/api-key-sanitize.js";
|
|
16
16
|
import { getMergedCatalog } from "./model-catalog/index.js";
|
|
17
|
+
import { mutateJsonFile } from "./utils/file-mutex.js";
|
|
17
18
|
// ProviderDef 类型 + PROVIDERS 目录已外移到 data/model-metadata.json
|
|
18
19
|
// (loader: data/model-metadata.ts),并在文件顶部 re-export。core 只读目录数据。
|
|
19
20
|
// ─── Dynamic model list (OpenRouter snapshot) ────────────────────
|
|
@@ -115,7 +116,7 @@ export async function validateApiKey(baseUrl, apiKey) {
|
|
|
115
116
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
116
117
|
signal: AbortSignal.timeout(10_000),
|
|
117
118
|
});
|
|
118
|
-
const data = await res.json();
|
|
119
|
+
const data = (await res.json());
|
|
119
120
|
return !data.error;
|
|
120
121
|
}
|
|
121
122
|
const url = baseUrl.replace(/\/$/, "") + "/models";
|
|
@@ -162,6 +163,8 @@ export function hasApiKey() {
|
|
|
162
163
|
// Reads ~/.code-shell/ only — ~/.claude/ compat was dropped because Claude
|
|
163
164
|
// Code's settings schema diverges and merging broke boot.
|
|
164
165
|
const p = join(userHome(), ".code-shell", "settings.json");
|
|
166
|
+
// This is only a boot-time hint. A slightly stale read can show onboarding
|
|
167
|
+
// once; all mutations below still re-read under the shared file lock.
|
|
165
168
|
if (existsSync(p)) {
|
|
166
169
|
try {
|
|
167
170
|
const data = JSON.parse(readFileSync(p, "utf-8"));
|
|
@@ -170,7 +173,9 @@ export function hasApiKey() {
|
|
|
170
173
|
if (Array.isArray(data?.modelConnections) && data.modelConnections.length > 0)
|
|
171
174
|
return true;
|
|
172
175
|
}
|
|
173
|
-
catch {
|
|
176
|
+
catch {
|
|
177
|
+
/* ignore */
|
|
178
|
+
}
|
|
174
179
|
}
|
|
175
180
|
return false;
|
|
176
181
|
}
|
|
@@ -297,50 +302,68 @@ export function appendOnboardingResult(opts) {
|
|
|
297
302
|
const tag = opts.tag ?? "text";
|
|
298
303
|
const dir = join(userHome(), ".code-shell");
|
|
299
304
|
const file = join(dir, "settings.json");
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
305
|
+
mutateJsonFile(file, {
|
|
306
|
+
parse: (raw) => {
|
|
307
|
+
if (raw === undefined)
|
|
308
|
+
return {};
|
|
309
|
+
let parsed;
|
|
310
|
+
try {
|
|
311
|
+
parsed = JSON.parse(raw);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
throw new Error("settings.json is unreadable; onboarding did not overwrite it", {
|
|
315
|
+
cause: error,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
319
|
+
throw new Error("settings.json must contain a JSON object");
|
|
320
|
+
}
|
|
321
|
+
return parsed;
|
|
322
|
+
},
|
|
323
|
+
serialize: (value) => `${JSON.stringify(value, null, 2)}\n`,
|
|
324
|
+
mutation: (existing) => {
|
|
325
|
+
const creds = Array.isArray(existing.credentials)
|
|
326
|
+
? [...existing.credentials]
|
|
327
|
+
: [];
|
|
328
|
+
const conns = Array.isArray(existing.modelConnections)
|
|
329
|
+
? [...existing.modelConnections]
|
|
330
|
+
: [];
|
|
331
|
+
for (const model of opts.models) {
|
|
332
|
+
const catalogId = catalogIdForKind(model.kind);
|
|
333
|
+
const credentialId = `${model.instanceId}-key`;
|
|
334
|
+
if (!creds.some((credential) => credential?.id === credentialId)) {
|
|
335
|
+
creds.push({
|
|
336
|
+
id: credentialId,
|
|
337
|
+
catalogId,
|
|
338
|
+
apiKey: model.apiKey,
|
|
339
|
+
baseUrl: model.baseUrl,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
if (!conns.some((connection) => connection?.id === model.instanceId)) {
|
|
343
|
+
conns.push({
|
|
344
|
+
id: model.instanceId,
|
|
345
|
+
catalogId,
|
|
346
|
+
tag,
|
|
347
|
+
model: model.model,
|
|
348
|
+
credentialId,
|
|
349
|
+
...(model.baseUrl ? { baseUrl: model.baseUrl } : {}),
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const existingDefaults = existing.defaults &&
|
|
354
|
+
typeof existing.defaults === "object" &&
|
|
355
|
+
!Array.isArray(existing.defaults)
|
|
356
|
+
? existing.defaults
|
|
357
|
+
: {};
|
|
358
|
+
return {
|
|
359
|
+
value: {
|
|
360
|
+
...existing,
|
|
361
|
+
credentials: creds,
|
|
362
|
+
modelConnections: conns,
|
|
363
|
+
defaults: { ...existingDefaults, [tag]: opts.activeId },
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
},
|
|
367
|
+
mode: 0o600,
|
|
368
|
+
});
|
|
346
369
|
}
|
|
@@ -57,37 +57,37 @@ export declare const PanelAppAgentContribution: z.ZodEffects<z.ZodObject<{
|
|
|
57
57
|
}>, "many">>;
|
|
58
58
|
skills: z.ZodDefault<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
59
59
|
}, "strict", z.ZodTypeAny, {
|
|
60
|
-
skills: string[];
|
|
61
60
|
tools: {
|
|
62
61
|
name: string;
|
|
63
62
|
description: string;
|
|
64
63
|
inputSchema: Record<string, unknown>;
|
|
65
64
|
readOnly: boolean;
|
|
66
65
|
}[];
|
|
66
|
+
skills: string[];
|
|
67
67
|
}, {
|
|
68
|
-
skills?: string[] | undefined;
|
|
69
68
|
tools?: {
|
|
70
69
|
name: string;
|
|
71
70
|
description: string;
|
|
72
71
|
inputSchema: Record<string, unknown>;
|
|
73
72
|
readOnly?: boolean | undefined;
|
|
74
73
|
}[] | undefined;
|
|
74
|
+
skills?: string[] | undefined;
|
|
75
75
|
}>, {
|
|
76
|
-
skills: string[];
|
|
77
76
|
tools: {
|
|
78
77
|
name: string;
|
|
79
78
|
description: string;
|
|
80
79
|
inputSchema: Record<string, unknown>;
|
|
81
80
|
readOnly: boolean;
|
|
82
81
|
}[];
|
|
82
|
+
skills: string[];
|
|
83
83
|
}, {
|
|
84
|
-
skills?: string[] | undefined;
|
|
85
84
|
tools?: {
|
|
86
85
|
name: string;
|
|
87
86
|
description: string;
|
|
88
87
|
inputSchema: Record<string, unknown>;
|
|
89
88
|
readOnly?: boolean | undefined;
|
|
90
89
|
}[] | undefined;
|
|
90
|
+
skills?: string[] | undefined;
|
|
91
91
|
}>;
|
|
92
92
|
/**
|
|
93
93
|
* Schema v2 keeps one installable Panel App identity while allowing it to
|
|
@@ -178,37 +178,37 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
|
|
|
178
178
|
}>, "many">>;
|
|
179
179
|
skills: z.ZodDefault<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
180
180
|
}, "strict", z.ZodTypeAny, {
|
|
181
|
-
skills: string[];
|
|
182
181
|
tools: {
|
|
183
182
|
name: string;
|
|
184
183
|
description: string;
|
|
185
184
|
inputSchema: Record<string, unknown>;
|
|
186
185
|
readOnly: boolean;
|
|
187
186
|
}[];
|
|
187
|
+
skills: string[];
|
|
188
188
|
}, {
|
|
189
|
-
skills?: string[] | undefined;
|
|
190
189
|
tools?: {
|
|
191
190
|
name: string;
|
|
192
191
|
description: string;
|
|
193
192
|
inputSchema: Record<string, unknown>;
|
|
194
193
|
readOnly?: boolean | undefined;
|
|
195
194
|
}[] | undefined;
|
|
195
|
+
skills?: string[] | undefined;
|
|
196
196
|
}>, {
|
|
197
|
-
skills: string[];
|
|
198
197
|
tools: {
|
|
199
198
|
name: string;
|
|
200
199
|
description: string;
|
|
201
200
|
inputSchema: Record<string, unknown>;
|
|
202
201
|
readOnly: boolean;
|
|
203
202
|
}[];
|
|
203
|
+
skills: string[];
|
|
204
204
|
}, {
|
|
205
|
-
skills?: string[] | undefined;
|
|
206
205
|
tools?: {
|
|
207
206
|
name: string;
|
|
208
207
|
description: string;
|
|
209
208
|
inputSchema: Record<string, unknown>;
|
|
210
209
|
readOnly?: boolean | undefined;
|
|
211
210
|
}[] | undefined;
|
|
211
|
+
skills?: string[] | undefined;
|
|
212
212
|
}>>;
|
|
213
213
|
id: z.ZodString;
|
|
214
214
|
version: z.ZodString;
|
|
@@ -247,13 +247,13 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
|
|
|
247
247
|
placement: "right-dock";
|
|
248
248
|
singleton: boolean;
|
|
249
249
|
agent?: {
|
|
250
|
-
skills: string[];
|
|
251
250
|
tools: {
|
|
252
251
|
name: string;
|
|
253
252
|
description: string;
|
|
254
253
|
inputSchema: Record<string, unknown>;
|
|
255
254
|
readOnly: boolean;
|
|
256
255
|
}[];
|
|
256
|
+
skills: string[];
|
|
257
257
|
} | undefined;
|
|
258
258
|
description?: string | undefined;
|
|
259
259
|
}, {
|
|
@@ -267,13 +267,13 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
|
|
|
267
267
|
};
|
|
268
268
|
schemaVersion: 2;
|
|
269
269
|
agent?: {
|
|
270
|
-
skills?: string[] | undefined;
|
|
271
270
|
tools?: {
|
|
272
271
|
name: string;
|
|
273
272
|
description: string;
|
|
274
273
|
inputSchema: Record<string, unknown>;
|
|
275
274
|
readOnly?: boolean | undefined;
|
|
276
275
|
}[] | undefined;
|
|
276
|
+
skills?: string[] | undefined;
|
|
277
277
|
} | undefined;
|
|
278
278
|
description?: string | undefined;
|
|
279
279
|
permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "agent.task" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage" | "process")[] | undefined;
|
|
@@ -310,13 +310,13 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
|
|
|
310
310
|
placement: "right-dock";
|
|
311
311
|
singleton: boolean;
|
|
312
312
|
agent?: {
|
|
313
|
-
skills: string[];
|
|
314
313
|
tools: {
|
|
315
314
|
name: string;
|
|
316
315
|
description: string;
|
|
317
316
|
inputSchema: Record<string, unknown>;
|
|
318
317
|
readOnly: boolean;
|
|
319
318
|
}[];
|
|
319
|
+
skills: string[];
|
|
320
320
|
} | undefined;
|
|
321
321
|
description?: string | undefined;
|
|
322
322
|
}, {
|
|
@@ -345,13 +345,13 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
|
|
|
345
345
|
};
|
|
346
346
|
schemaVersion: 2;
|
|
347
347
|
agent?: {
|
|
348
|
-
skills?: string[] | undefined;
|
|
349
348
|
tools?: {
|
|
350
349
|
name: string;
|
|
351
350
|
description: string;
|
|
352
351
|
inputSchema: Record<string, unknown>;
|
|
353
352
|
readOnly?: boolean | undefined;
|
|
354
353
|
}[] | undefined;
|
|
354
|
+
skills?: string[] | undefined;
|
|
355
355
|
} | undefined;
|
|
356
356
|
description?: string | undefined;
|
|
357
357
|
permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "agent.task" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage" | "process")[] | undefined;
|