@code-yeongyu/senpi-ai 2026.10.30-2 → 2026.10.31-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/anthropic-messages.d.ts +13 -3
- package/dist/api/anthropic-messages.js +62 -185
- package/dist/api/anthropic-system-blocks.d.ts +12 -0
- package/dist/api/anthropic-system-blocks.js +35 -0
- package/dist/api/openai-completions.js +19 -2
- package/dist/auth/helpers.d.ts +1 -0
- package/dist/auth/helpers.js +1 -0
- package/dist/auth/oauth/load.d.ts +0 -2
- package/dist/auth/oauth/load.js +0 -5
- package/dist/auth/pool/slots.d.ts +11 -1
- package/dist/auth/pool/slots.js +63 -1
- package/dist/auth/types.d.ts +7 -0
- package/dist/bun-oauth.js +0 -2
- package/dist/compat/extension-oauth-types.d.ts +1 -0
- package/dist/models.js +1 -1
- package/dist/oauth.d.ts +1 -1
- package/dist/oauth.js +1 -1
- package/dist/providers/anthropic.js +2 -1
- package/dist/providers/chatgpt-subscription.js +2 -0
- package/dist/providers/data/.manifest.json +1 -1
- package/dist/providers/data/cloudflare-ai-gateway.json +1 -1
- package/dist/providers/data/opengateway.json +1 -1
- package/dist/providers/data/openrouter.json +1 -1
- package/dist/providers/data/venice.json +1 -1
- package/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/dist/types.d.ts +16 -0
- package/dist/utils/chatgpt-subscription-auth.d.ts +7 -0
- package/dist/utils/chatgpt-subscription-auth.js +21 -4
- package/dist/utils/prompt-cache-ttl.d.ts +2 -1
- package/dist/utils/prompt-cache-ttl.js +2 -0
- package/dist/utils/retry-hint.d.ts +3 -1
- package/dist/utils/retry-hint.js +80 -2
- package/dist/utils/tool-schema-compat.js +35 -0
- package/dist/utils/transcript.d.ts +1 -1
- package/dist/utils/transcript.js +12 -2
- package/dist/utils/validation.js +111 -4
- package/package.json +2 -2
- package/dist/auth/oauth/anthropic-callback-listener.d.ts +0 -40
- package/dist/auth/oauth/anthropic-callback-listener.js +0 -138
- package/dist/auth/oauth/anthropic.d.ts +0 -13
- package/dist/auth/oauth/anthropic.js +0 -285
- package/dist/auth/oauth/authorization-input.d.ts +0 -6
- package/dist/auth/oauth/authorization-input.js +0 -29
- package/dist/auth/oauth/error-details.d.ts +0 -3
- package/dist/auth/oauth/error-details.js +0 -20
- package/dist/utils/claude-code-version-cache.d.ts +0 -12
- package/dist/utils/claude-code-version-cache.js +0 -60
- package/dist/utils/claude-code-version.d.ts +0 -78
- package/dist/utils/claude-code-version.js +0 -203
package/dist/types.d.ts
CHANGED
|
@@ -513,6 +513,14 @@ export interface SystemMessage {
|
|
|
513
513
|
* the original. Avoid integer-like names; JSON objects reorder those.
|
|
514
514
|
*/
|
|
515
515
|
sections?: Record<string, string | null>;
|
|
516
|
+
/**
|
|
517
|
+
* Leading message only: the start of the rendered prompt that is the same across sessions in the same
|
|
518
|
+
* working directory. Providers with block-level prompt caching (Anthropic) send it as its own block
|
|
519
|
+
* holding the system cache breakpoint when the rendered prompt still starts with it, so another session
|
|
520
|
+
* reads it from cache even when the rest differs; a rewrite that no longer starts with it sends one block.
|
|
521
|
+
* Request-time only; rendered text is unchanged (senpi#4447).
|
|
522
|
+
*/
|
|
523
|
+
stablePrefix?: string;
|
|
516
524
|
/** Complete definitions of tools that become available at this point. */
|
|
517
525
|
toolsAdded?: Tool[];
|
|
518
526
|
/** Tools that stop being available at this point. */
|
|
@@ -769,6 +777,12 @@ export interface ToolReference {
|
|
|
769
777
|
*/
|
|
770
778
|
export interface Context {
|
|
771
779
|
systemPrompt?: string;
|
|
780
|
+
/**
|
|
781
|
+
* Leading part of `systemPrompt` that is the same across sessions in the same working directory; the rest
|
|
782
|
+
* changes between sessions (memory, MCP instructions). Folded into the leading system message as
|
|
783
|
+
* `stablePrefix` when `systemPrompt` starts with it (senpi#4447).
|
|
784
|
+
*/
|
|
785
|
+
systemPromptStablePrefix?: string;
|
|
772
786
|
messages: Message[];
|
|
773
787
|
tools?: Tool[];
|
|
774
788
|
/**
|
|
@@ -941,6 +955,8 @@ export interface OpenAICompletionsCompat {
|
|
|
941
955
|
supportsMidConvoToolAdditions?: boolean;
|
|
942
956
|
/** Whether the provider supports the `strict` field in tool definitions. Default: false; generated capable models enable it explicitly. */
|
|
943
957
|
supportsStrictMode?: boolean;
|
|
958
|
+
/** Whether function tools may mix strict and non-strict sampling in one request. Default: true. */
|
|
959
|
+
mixedStrictTools?: boolean;
|
|
944
960
|
/**
|
|
945
961
|
* Provider-specific JSON Schema flavor for tool parameters. `"moonshot-mfjs"`
|
|
946
962
|
* normalizes schemas for Moonshot / Kimi backends that enforce a stricter subset.
|
|
@@ -8,4 +8,11 @@ export declare const CHATGPT_SUBSCRIPTION_AUTH_CLAIM_PATH = "https://api.openai.
|
|
|
8
8
|
* browser consumers.
|
|
9
9
|
*/
|
|
10
10
|
export declare function extractChatGptSubscriptionAccountId(token: string): string | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* The signed-in person a Codex access token belongs to, for telling one saved account from another.
|
|
13
|
+
* `chatgpt_account_id` names the workspace and is shared by every member of a Team workspace, so the
|
|
14
|
+
* identity is the per-member `chatgpt_account_user_id`, or the user id scoped to the workspace when
|
|
15
|
+
* that claim is absent. A token without either has no identity.
|
|
16
|
+
*/
|
|
17
|
+
export declare function extractChatGptSubscriptionIdentity(token: string): string | undefined;
|
|
11
18
|
//# sourceMappingURL=chatgpt-subscription-auth.d.ts.map
|
|
@@ -8,6 +8,22 @@ export const CHATGPT_SUBSCRIPTION_AUTH_CLAIM_PATH = "https://api.openai.com/auth
|
|
|
8
8
|
* browser consumers.
|
|
9
9
|
*/
|
|
10
10
|
export function extractChatGptSubscriptionAccountId(token) {
|
|
11
|
+
return claimString(authClaims(token), "chatgpt_account_id");
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The signed-in person a Codex access token belongs to, for telling one saved account from another.
|
|
15
|
+
* `chatgpt_account_id` names the workspace and is shared by every member of a Team workspace, so the
|
|
16
|
+
* identity is the per-member `chatgpt_account_user_id`, or the user id scoped to the workspace when
|
|
17
|
+
* that claim is absent. A token without either has no identity.
|
|
18
|
+
*/
|
|
19
|
+
export function extractChatGptSubscriptionIdentity(token) {
|
|
20
|
+
const auth = authClaims(token);
|
|
21
|
+
const workspace = claimString(auth, "chatgpt_account_id");
|
|
22
|
+
const user = claimString(auth, "chatgpt_user_id") ?? claimString(auth, "user_id");
|
|
23
|
+
return (claimString(auth, "chatgpt_account_user_id") ??
|
|
24
|
+
(user !== undefined && workspace !== undefined ? `${user}/${workspace}` : undefined));
|
|
25
|
+
}
|
|
26
|
+
function authClaims(token) {
|
|
11
27
|
try {
|
|
12
28
|
const parts = token.split(".");
|
|
13
29
|
const encodedPayload = parts.length === 3 ? parts[1] : undefined;
|
|
@@ -18,13 +34,14 @@ export function extractChatGptSubscriptionAccountId(token) {
|
|
|
18
34
|
if (!payload || typeof payload !== "object")
|
|
19
35
|
return undefined;
|
|
20
36
|
const auth = Reflect.get(payload, CHATGPT_SUBSCRIPTION_AUTH_CLAIM_PATH);
|
|
21
|
-
|
|
22
|
-
return undefined;
|
|
23
|
-
const accountId = Reflect.get(auth, "chatgpt_account_id");
|
|
24
|
-
return typeof accountId === "string" && accountId.length > 0 ? accountId : undefined;
|
|
37
|
+
return auth && typeof auth === "object" ? auth : undefined;
|
|
25
38
|
}
|
|
26
39
|
catch {
|
|
27
40
|
return undefined;
|
|
28
41
|
}
|
|
29
42
|
}
|
|
43
|
+
function claimString(claims, key) {
|
|
44
|
+
const value = claims === undefined ? undefined : Reflect.get(claims, key);
|
|
45
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
46
|
+
}
|
|
30
47
|
//# sourceMappingURL=chatgpt-subscription-auth.js.map
|
|
@@ -24,7 +24,8 @@ export type PromptCacheLifetime = {
|
|
|
24
24
|
};
|
|
25
25
|
export declare function isAnthropicApiBaseUrl(baseUrl: string): boolean;
|
|
26
26
|
export declare function getAnthropicCompat(model: Model<"anthropic-messages">): Required<Omit<AnthropicMessagesCompat, "forceAdaptiveThinking" | "supportsMidConvoEffort" | "sessionAffinityFormat">> & Pick<AnthropicMessagesCompat, "sessionAffinityFormat">;
|
|
27
|
-
export type ResolvedOpenAICompletionsCompat = Omit<Required<OpenAICompletionsCompat>, "vllmPriority" | "cacheControlFormat" | "toolCallFormat" | "deferredToolsMode" | "toolSchemaFlavor" | "supportsPromptCacheKey" | "chatTemplateArgs" | "supportsThinkingTokenBudget" | "thinkingTokenBudgetField" | "veniceParameters" | "supportsForcedToolChoice" | "supportsWebSearch"> & {
|
|
27
|
+
export type ResolvedOpenAICompletionsCompat = Omit<Required<OpenAICompletionsCompat>, "vllmPriority" | "cacheControlFormat" | "toolCallFormat" | "deferredToolsMode" | "toolSchemaFlavor" | "supportsPromptCacheKey" | "chatTemplateArgs" | "supportsThinkingTokenBudget" | "thinkingTokenBudgetField" | "veniceParameters" | "supportsForcedToolChoice" | "supportsWebSearch" | "mixedStrictTools"> & {
|
|
28
|
+
mixedStrictTools?: OpenAICompletionsCompat["mixedStrictTools"];
|
|
28
29
|
supportsWebSearch?: OpenAICompletionsCompat["supportsWebSearch"];
|
|
29
30
|
/** Declared forced tool_choice support; absent means supported. */
|
|
30
31
|
supportsForcedToolChoice?: OpenAICompletionsCompat["supportsForcedToolChoice"];
|
|
@@ -161,6 +161,7 @@ function detectOpenAICompletionsCompat(model) {
|
|
|
161
161
|
zaiToolStream: false,
|
|
162
162
|
// OpenAI compatibility alone does not imply strict JSON-schema tool support.
|
|
163
163
|
supportsStrictMode: false,
|
|
164
|
+
mixedStrictTools: true,
|
|
164
165
|
toolSchemaFlavor: isMoonshot ? "moonshot-mfjs" : undefined,
|
|
165
166
|
supportsDisabledThinking: true,
|
|
166
167
|
toolCallFormat: undefined,
|
|
@@ -211,6 +212,7 @@ export function getOpenAICompletionsCompat(model) {
|
|
|
211
212
|
supportsThinkingTokenBudget: model.compat.supportsThinkingTokenBudget ?? detected.supportsThinkingTokenBudget,
|
|
212
213
|
thinkingTokenBudgetField: model.compat.thinkingTokenBudgetField ?? detected.thinkingTokenBudgetField,
|
|
213
214
|
supportsStrictMode: model.compat.supportsStrictMode ?? detected.supportsStrictMode,
|
|
215
|
+
mixedStrictTools: model.compat.mixedStrictTools ?? detected.mixedStrictTools,
|
|
214
216
|
toolSchemaFlavor: model.compat.toolSchemaFlavor ?? detected.toolSchemaFlavor,
|
|
215
217
|
toolCallFormat: model.compat.toolCallFormat ?? detected.toolCallFormat,
|
|
216
218
|
supportsOpenAIGrammarTools: model.compat.supportsOpenAIGrammarTools ?? detected.supportsOpenAIGrammarTools,
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* 2. retry-after header (strict integer delta-seconds `^\d+$`, else HTTP-date)
|
|
7
7
|
* 3. x-ratelimit-reset{,-requests,-tokens} (epoch seconds, max-wins)
|
|
8
8
|
* 4. JSON retryDelay strings (ms/s/m units, recursive, max-wins; numeric rejected)
|
|
9
|
-
* 5. body prose (ms-marker, seconds-marker, relative, resets-in, resets-at ISO8601
|
|
9
|
+
* 5. body prose (ms-marker, seconds-marker, relative, resets-in, resets-at ISO8601,
|
|
10
|
+
* wall clock with an optional zone: "resets 12am (Asia/Seoul)")
|
|
10
11
|
*
|
|
11
12
|
* Eligibility gate: status 429 OR body marks rate-limit error.
|
|
12
13
|
* Explicit zero = 0 (beats lower-precedence positives).
|
|
@@ -21,4 +22,5 @@ export interface RetryHintInput {
|
|
|
21
22
|
export declare function extract429RetryAfterMs(input: RetryHintInput, nowMs?: number): number | undefined;
|
|
22
23
|
export declare function appendRetryAfterMsMarker(message: string, hintMs: number): string;
|
|
23
24
|
export declare function parseRetryAfterMsMarker(message: string): number | undefined;
|
|
25
|
+
export declare function clockResetMs(text: string, nowMs: number): number | undefined;
|
|
24
26
|
//# sourceMappingURL=retry-hint.d.ts.map
|
package/dist/utils/retry-hint.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* 2. retry-after header (strict integer delta-seconds `^\d+$`, else HTTP-date)
|
|
7
7
|
* 3. x-ratelimit-reset{,-requests,-tokens} (epoch seconds, max-wins)
|
|
8
8
|
* 4. JSON retryDelay strings (ms/s/m units, recursive, max-wins; numeric rejected)
|
|
9
|
-
* 5. body prose (ms-marker, seconds-marker, relative, resets-in, resets-at ISO8601
|
|
9
|
+
* 5. body prose (ms-marker, seconds-marker, relative, resets-in, resets-at ISO8601,
|
|
10
|
+
* wall clock with an optional zone: "resets 12am (Asia/Seoul)")
|
|
10
11
|
*
|
|
11
12
|
* Eligibility gate: status 429 OR body marks rate-limit error.
|
|
12
13
|
* Explicit zero = 0 (beats lower-precedence positives).
|
|
@@ -145,7 +146,8 @@ function fromBodyProse(bodyText, nowMs) {
|
|
|
145
146
|
if (!Number.isNaN(parsed))
|
|
146
147
|
return clampAbsolute(parsed, nowMs);
|
|
147
148
|
}
|
|
148
|
-
|
|
149
|
+
// resets <wall clock> [(<IANA zone>)], the shape Claude Code writes (senpi#1771)
|
|
150
|
+
return clockResetMs(bodyText, nowMs);
|
|
149
151
|
}
|
|
150
152
|
/* ---------- helpers ---------- */
|
|
151
153
|
function dateHeaderMs(h) {
|
|
@@ -234,4 +236,80 @@ export function parseRetryAfterMsMarker(message) {
|
|
|
234
236
|
return undefined;
|
|
235
237
|
return Number.parseInt(m[1], 10);
|
|
236
238
|
}
|
|
239
|
+
/* ---------- wall-clock reset prose ---------- */
|
|
240
|
+
// A reset written as a wall clock: "resets 12am (Asia/Seoul)", "resets Oct 2, 9am", "try again at 12:00 AM".
|
|
241
|
+
// Shared with the credential pool (reset-time.ts) so both read the text the same way (senpi#1771). Milliseconds
|
|
242
|
+
// from `nowMs` to the next such moment, or undefined; a time with no zone reads in the host's local zone.
|
|
243
|
+
const MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
|
244
|
+
const CLOCK_RE = /\b(?:resets?|try again)\s+(?:at\s+|on\s+)?(?:([a-z]{3})[a-z]*\.?\s+(\d{1,2}),?\s+(?:at\s+)?)?(\d{1,2})(?::(\d{2}))?\s*([ap])\.?\s?m\b\.?(?:\s*\(([A-Za-z_]+(?:\/[A-Za-z0-9_+-]+)*)\))?/i;
|
|
245
|
+
export function clockResetMs(text, nowMs) {
|
|
246
|
+
const match = CLOCK_RE.exec(text);
|
|
247
|
+
if (!match)
|
|
248
|
+
return undefined;
|
|
249
|
+
const [, monthName, dayText, hourText, minuteText, meridiem, zoneText] = match;
|
|
250
|
+
const hour12 = Number.parseInt(hourText ?? "", 10);
|
|
251
|
+
const minute = minuteText === undefined ? 0 : Number.parseInt(minuteText, 10);
|
|
252
|
+
if (hour12 < 1 || hour12 > 12 || minute > 59)
|
|
253
|
+
return undefined;
|
|
254
|
+
const hour = (hour12 % 12) + (meridiem?.toLowerCase() === "p" ? 12 : 0);
|
|
255
|
+
const zone = zoneText ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
256
|
+
if (!isValidTimeZone(zone))
|
|
257
|
+
return undefined;
|
|
258
|
+
const today = zonedParts(zone, nowMs);
|
|
259
|
+
if (monthName !== undefined) {
|
|
260
|
+
const month = MONTHS.indexOf(monthName.toLowerCase());
|
|
261
|
+
const day = Number.parseInt(dayText ?? "", 10);
|
|
262
|
+
if (month < 0 || day < 1 || day > 31)
|
|
263
|
+
return undefined;
|
|
264
|
+
let target = zonedToEpoch(zone, today.year, month, day, hour, minute);
|
|
265
|
+
if (target <= nowMs)
|
|
266
|
+
target = zonedToEpoch(zone, today.year + 1, month, day, hour, minute);
|
|
267
|
+
return target - nowMs;
|
|
268
|
+
}
|
|
269
|
+
let target = zonedToEpoch(zone, today.year, today.month, today.day, hour, minute);
|
|
270
|
+
if (target <= nowMs)
|
|
271
|
+
target = zonedToEpoch(zone, today.year, today.month, today.day + 1, hour, minute);
|
|
272
|
+
return target - nowMs;
|
|
273
|
+
}
|
|
274
|
+
function isValidTimeZone(zone) {
|
|
275
|
+
try {
|
|
276
|
+
new Intl.DateTimeFormat("en-US", { timeZone: zone });
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function zonedParts(zone, epochMs) {
|
|
284
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
285
|
+
timeZone: zone,
|
|
286
|
+
hourCycle: "h23",
|
|
287
|
+
year: "numeric",
|
|
288
|
+
month: "numeric",
|
|
289
|
+
day: "numeric",
|
|
290
|
+
hour: "numeric",
|
|
291
|
+
minute: "numeric",
|
|
292
|
+
second: "numeric",
|
|
293
|
+
}).formatToParts(epochMs);
|
|
294
|
+
const get = (type) => Number.parseInt(parts.find((part) => part.type === type)?.value ?? "0", 10);
|
|
295
|
+
return {
|
|
296
|
+
year: get("year"),
|
|
297
|
+
month: get("month") - 1,
|
|
298
|
+
day: get("day"),
|
|
299
|
+
hour: get("hour"),
|
|
300
|
+
minute: get("minute"),
|
|
301
|
+
second: get("second"),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function zoneOffsetMs(zone, epochMs) {
|
|
305
|
+
const p = zonedParts(zone, epochMs);
|
|
306
|
+
const wallAsUtc = Date.UTC(p.year, p.month, p.day, p.hour, p.minute, p.second);
|
|
307
|
+
return wallAsUtc - Math.floor(epochMs / 1000) * 1000;
|
|
308
|
+
}
|
|
309
|
+
/** The instant a wall-clock time in `zone` names; two passes settle a DST boundary. */
|
|
310
|
+
function zonedToEpoch(zone, year, month, day, hour, minute) {
|
|
311
|
+
const wall = Date.UTC(year, month, day, hour, minute);
|
|
312
|
+
const first = wall - zoneOffsetMs(zone, wall);
|
|
313
|
+
return wall - zoneOffsetMs(zone, first);
|
|
314
|
+
}
|
|
237
315
|
//# sourceMappingURL=retry-hint.js.map
|
|
@@ -148,16 +148,51 @@ function mergeRootObjectUnion(schema, combiner) {
|
|
|
148
148
|
...(selected === "allOf" ? branchRequiredSets.flatMap((names) => [...names]) : commonBranchRequired),
|
|
149
149
|
]),
|
|
150
150
|
];
|
|
151
|
+
// A union's merged root drops each branch's own required names; keep them visible to the model (#5138).
|
|
152
|
+
const hints = selected === "allOf"
|
|
153
|
+
? []
|
|
154
|
+
: objectBranches.flatMap((branch, index) => {
|
|
155
|
+
const own = [...(branchRequiredSets[index] ?? [])].filter((name) => !required.includes(name));
|
|
156
|
+
return own.length === 0 ? [] : [`REQUIRED for ${describeBranch(branch, index)}: ${own.join(", ")}.`];
|
|
157
|
+
});
|
|
158
|
+
const description = hints.length === 0
|
|
159
|
+
? undefined
|
|
160
|
+
: [typeof schema.description === "string" ? schema.description : "", ...hints].filter(Boolean).join(" ");
|
|
151
161
|
const rest = { ...schema };
|
|
152
162
|
for (const key of combiner ? [combiner] : ["anyOf", "oneOf"])
|
|
153
163
|
delete rest[key];
|
|
154
164
|
return {
|
|
155
165
|
...rest,
|
|
166
|
+
...(description === undefined ? {} : { description }),
|
|
156
167
|
type: "object",
|
|
157
168
|
properties,
|
|
158
169
|
...(required.length > 0 ? { required } : {}),
|
|
159
170
|
};
|
|
160
171
|
}
|
|
172
|
+
/** Names a union branch by its first literal-valued property (`action=run`), else by position. */
|
|
173
|
+
function describeBranch(branch, index) {
|
|
174
|
+
if (isJsonObject(branch.properties)) {
|
|
175
|
+
for (const [name, propertySchema] of Object.entries(branch.properties)) {
|
|
176
|
+
const values = literalValues(propertySchema);
|
|
177
|
+
if (values.length > 0)
|
|
178
|
+
return `${name}=${values.join("|")}`;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return `option ${index + 1}`;
|
|
182
|
+
}
|
|
183
|
+
function literalValues(schema) {
|
|
184
|
+
if (!isJsonObject(schema))
|
|
185
|
+
return [];
|
|
186
|
+
if (Object.hasOwn(schema, "const"))
|
|
187
|
+
return [String(schema.const)];
|
|
188
|
+
if (Array.isArray(schema.enum))
|
|
189
|
+
return schema.enum.map(String);
|
|
190
|
+
const branches = schema.anyOf;
|
|
191
|
+
if (!Array.isArray(branches))
|
|
192
|
+
return [];
|
|
193
|
+
const values = branches.map((branch) => literalValues(branch));
|
|
194
|
+
return values.every((value) => value.length === 1) ? values.flat() : [];
|
|
195
|
+
}
|
|
161
196
|
function normalizeNode(node, isRoot = false) {
|
|
162
197
|
if (Array.isArray(node)) {
|
|
163
198
|
return node.map((child) => normalizeNode(child));
|
|
@@ -4,7 +4,7 @@ export type { TranscriptContext } from "../types.ts";
|
|
|
4
4
|
* Build the leading system message for a prompt and tool set. Returns undefined when
|
|
5
5
|
* both are empty, so an empty transcript stays empty.
|
|
6
6
|
*/
|
|
7
|
-
export declare function createInitialSystemMessage(systemPrompt: string | undefined, tools: Tool[] | undefined): SystemMessage | undefined;
|
|
7
|
+
export declare function createInitialSystemMessage(systemPrompt: string | undefined, tools: Tool[] | undefined, stablePrefix?: string): SystemMessage | undefined;
|
|
8
8
|
/**
|
|
9
9
|
* Fold `Context.systemPrompt` and `Context.tools` into a leading system message.
|
|
10
10
|
* This is the only entry point that produces a {@link TranscriptContext}; every
|
package/dist/utils/transcript.js
CHANGED
|
@@ -3,14 +3,20 @@ import { contentText, getSystemMessageText } from "./text.js";
|
|
|
3
3
|
* Build the leading system message for a prompt and tool set. Returns undefined when
|
|
4
4
|
* both are empty, so an empty transcript stays empty.
|
|
5
5
|
*/
|
|
6
|
-
export function createInitialSystemMessage(systemPrompt, tools) {
|
|
6
|
+
export function createInitialSystemMessage(systemPrompt, tools, stablePrefix) {
|
|
7
7
|
const hasSystemPrompt = systemPrompt !== undefined && systemPrompt.length > 0;
|
|
8
8
|
const hasTools = tools !== undefined && tools.length > 0;
|
|
9
9
|
if (!hasSystemPrompt && !hasTools)
|
|
10
10
|
return undefined;
|
|
11
|
+
const splitsPrompt = hasSystemPrompt &&
|
|
12
|
+
stablePrefix !== undefined &&
|
|
13
|
+
stablePrefix.length > 0 &&
|
|
14
|
+
stablePrefix.length < systemPrompt.length &&
|
|
15
|
+
systemPrompt.startsWith(stablePrefix);
|
|
11
16
|
return {
|
|
12
17
|
role: "system",
|
|
13
18
|
content: systemPrompt ?? "",
|
|
19
|
+
...(splitsPrompt ? { stablePrefix } : {}),
|
|
14
20
|
...(hasTools ? { toolsAdded: tools } : {}),
|
|
15
21
|
timestamp: 0,
|
|
16
22
|
};
|
|
@@ -22,7 +28,7 @@ export function createInitialSystemMessage(systemPrompt, tools) {
|
|
|
22
28
|
* over unchanged (senpi#2095 allowed-tools): it has no system-message representation.
|
|
23
29
|
*/
|
|
24
30
|
export function normalizeContext(context) {
|
|
25
|
-
const initialMessage = createInitialSystemMessage(context.systemPrompt, context.tools);
|
|
31
|
+
const initialMessage = createInitialSystemMessage(context.systemPrompt, context.tools, context.systemPromptStablePrefix);
|
|
26
32
|
const messages = initialMessage ? [initialMessage, ...context.messages] : context.messages;
|
|
27
33
|
return (context.activeToolNames ? { messages, activeToolNames: context.activeToolNames } : { messages });
|
|
28
34
|
}
|
|
@@ -60,9 +66,12 @@ export function getCurrentSystemMessage(messages) {
|
|
|
60
66
|
const content = [];
|
|
61
67
|
const sections = new Map();
|
|
62
68
|
let timestamp;
|
|
69
|
+
let stablePrefix;
|
|
63
70
|
for (const message of messages) {
|
|
64
71
|
if (!isSystemMessage(message))
|
|
65
72
|
continue;
|
|
73
|
+
if (timestamp === undefined)
|
|
74
|
+
stablePrefix = message.stablePrefix;
|
|
66
75
|
timestamp ??= message.timestamp;
|
|
67
76
|
const text = contentText(message.content);
|
|
68
77
|
if (text.length > 0)
|
|
@@ -80,6 +89,7 @@ export function getCurrentSystemMessage(messages) {
|
|
|
80
89
|
return {
|
|
81
90
|
role: "system",
|
|
82
91
|
content: content.join("\n\n"),
|
|
92
|
+
...(stablePrefix === undefined ? {} : { stablePrefix }),
|
|
83
93
|
...(sections.size > 0 ? { sections: Object.fromEntries(sections) } : {}),
|
|
84
94
|
...(tools.length > 0 ? { toolsAdded: tools } : {}),
|
|
85
95
|
timestamp: timestamp ?? 0,
|
package/dist/utils/validation.js
CHANGED
|
@@ -185,6 +185,111 @@ function formatValidationPath(error) {
|
|
|
185
185
|
const path = error.instancePath.replace(/^\//, "").replace(/\//g, ".");
|
|
186
186
|
return path || "root";
|
|
187
187
|
}
|
|
188
|
+
function constValues(schema) {
|
|
189
|
+
if (!schema)
|
|
190
|
+
return undefined;
|
|
191
|
+
if ("const" in schema)
|
|
192
|
+
return [schema.const];
|
|
193
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0)
|
|
194
|
+
return [...schema.enum];
|
|
195
|
+
const members = schema.anyOf ?? schema.oneOf;
|
|
196
|
+
if (!Array.isArray(members) || members.length === 0)
|
|
197
|
+
return undefined;
|
|
198
|
+
const values = [];
|
|
199
|
+
for (const member of members) {
|
|
200
|
+
const memberValues = constValues(member);
|
|
201
|
+
if (!memberValues)
|
|
202
|
+
return undefined;
|
|
203
|
+
values.push(...memberValues);
|
|
204
|
+
}
|
|
205
|
+
return values;
|
|
206
|
+
}
|
|
207
|
+
function discriminatedBranches(members, property) {
|
|
208
|
+
const seen = new Set();
|
|
209
|
+
const branches = [];
|
|
210
|
+
for (const member of members) {
|
|
211
|
+
const propertySchema = member.properties?.[property];
|
|
212
|
+
const values = constValues(propertySchema);
|
|
213
|
+
if (!values || values.some((value) => seen.has(value)))
|
|
214
|
+
return undefined;
|
|
215
|
+
for (const value of values)
|
|
216
|
+
seen.add(value);
|
|
217
|
+
branches.push({
|
|
218
|
+
schema: member,
|
|
219
|
+
values,
|
|
220
|
+
optional: !(member.required ?? []).includes(property),
|
|
221
|
+
defaultValue: propertySchema?.default,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return branches;
|
|
225
|
+
}
|
|
226
|
+
/** An anyOf/oneOf of object branches that every carry a const-valued property with disjoint values. */
|
|
227
|
+
function findDiscriminatedUnion(schema) {
|
|
228
|
+
const members = schema.anyOf ?? schema.oneOf;
|
|
229
|
+
if (!Array.isArray(members) || members.length < 2)
|
|
230
|
+
return undefined;
|
|
231
|
+
const [first] = members;
|
|
232
|
+
if (!first?.properties || !members.every((member) => member.properties !== undefined))
|
|
233
|
+
return undefined;
|
|
234
|
+
for (const property of Object.keys(first.properties)) {
|
|
235
|
+
const branches = discriminatedBranches(members, property);
|
|
236
|
+
if (!branches)
|
|
237
|
+
continue;
|
|
238
|
+
const defaultValue = schema.properties?.[property]?.default ??
|
|
239
|
+
branches.map((branch) => branch.defaultValue).find((value) => value !== undefined);
|
|
240
|
+
return { property, branches, defaultValue };
|
|
241
|
+
}
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
function selectBranch(union, value) {
|
|
245
|
+
if (value !== undefined) {
|
|
246
|
+
const branch = union.branches.find((candidate) => candidate.values.includes(value));
|
|
247
|
+
return branch ? { branch, value, defaulted: false } : "unknown";
|
|
248
|
+
}
|
|
249
|
+
if (union.defaultValue !== undefined) {
|
|
250
|
+
const branch = union.branches.find((candidate) => candidate.values.includes(union.defaultValue));
|
|
251
|
+
if (branch)
|
|
252
|
+
return { branch, value: union.defaultValue, defaulted: true };
|
|
253
|
+
}
|
|
254
|
+
const optional = union.branches.filter((candidate) => candidate.optional);
|
|
255
|
+
const [only] = optional;
|
|
256
|
+
if (optional.length === 1 && only && only.values.length === 1) {
|
|
257
|
+
return { branch: only, value: only.values[0], defaulted: true };
|
|
258
|
+
}
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* For a discriminated union, report only the branch the arguments address, as one line, so the caller
|
|
263
|
+
* sees which field to fix instead of every branch's mismatch. Undefined means use the full error list.
|
|
264
|
+
*/
|
|
265
|
+
function formatDiscriminatedUnionError(schema, validator, args) {
|
|
266
|
+
if (typeof args !== "object" || args === null || Array.isArray(args))
|
|
267
|
+
return undefined;
|
|
268
|
+
const union = findDiscriminatedUnion(schema);
|
|
269
|
+
if (!union)
|
|
270
|
+
return undefined;
|
|
271
|
+
const received = args[union.property];
|
|
272
|
+
const selected = selectBranch(union, received);
|
|
273
|
+
if (selected === undefined)
|
|
274
|
+
return undefined;
|
|
275
|
+
if (selected === "unknown") {
|
|
276
|
+
const allowed = union.branches.flatMap((branch) => branch.values).map((value) => JSON.stringify(value));
|
|
277
|
+
return ` - ${union.property}: must be one of ${allowed.join(", ")} (received ${JSON.stringify(received)})`;
|
|
278
|
+
}
|
|
279
|
+
const branchValidator = getSubSchemaValidator(selected.branch.schema);
|
|
280
|
+
if (!branchValidator)
|
|
281
|
+
return undefined;
|
|
282
|
+
const rootErrors = validator
|
|
283
|
+
.Errors(args)
|
|
284
|
+
.filter((error) => error.keyword !== "anyOf" && error.keyword !== "oneOf")
|
|
285
|
+
.filter((error) => !/^#\/(?:anyOf|oneOf)(?:\/|$)/.test(error.schemaPath));
|
|
286
|
+
const messages = [...rootErrors, ...branchValidator.Errors(args)].map((error) => `${formatValidationPath(error)}: ${error.message}`);
|
|
287
|
+
const unique = [...new Set(messages)];
|
|
288
|
+
if (unique.length === 0)
|
|
289
|
+
return undefined;
|
|
290
|
+
const label = `for ${union.property} ${JSON.stringify(selected.value)}${selected.defaulted ? ` (default when ${union.property} is omitted)` : ""}`;
|
|
291
|
+
return ` - ${label}: ${unique.join("; ")}`;
|
|
292
|
+
}
|
|
188
293
|
/**
|
|
189
294
|
* Finds a tool by name and validates the tool call arguments against its TypeBox schema
|
|
190
295
|
* @param tools Array of tool definitions
|
|
@@ -238,10 +343,12 @@ export function validateToolArguments(tool, toolCall) {
|
|
|
238
343
|
if (validator.Check(args)) {
|
|
239
344
|
return args;
|
|
240
345
|
}
|
|
241
|
-
const errors = validator
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
346
|
+
const errors = formatDiscriminatedUnionError(tool.parameters, validator, args) ??
|
|
347
|
+
(validator
|
|
348
|
+
.Errors(args)
|
|
349
|
+
.map((error) => ` - ${formatValidationPath(error)}: ${error.message}`)
|
|
350
|
+
.join("\n") ||
|
|
351
|
+
"Unknown validation error");
|
|
245
352
|
const errorMessage = `Validation failed for tool "${toolCall.name}":\n${errors}\n\nReceived arguments:\n${JSON.stringify(toolCall.arguments, null, 2)}`;
|
|
246
353
|
throw new Error(errorMessage);
|
|
247
354
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-ai",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.31-2",
|
|
4
4
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"dependencies": {
|
|
89
89
|
"@anthropic-ai/sdk": "0.127.0",
|
|
90
90
|
"@aws-sdk/client-bedrock-runtime": "3.1136.0",
|
|
91
|
-
"@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.10.
|
|
91
|
+
"@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.10.31-2",
|
|
92
92
|
"@google/genai": "2.23.0",
|
|
93
93
|
"@smithy/node-http-handler": "4.12.1",
|
|
94
94
|
"google-auth-library": "10.6.2",
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Loopback callback listener for the Anthropic OAuth flow.
|
|
3
|
-
*
|
|
4
|
-
* The OAuth client accepts any `localhost` port on the `/callback` path (the
|
|
5
|
-
* Claude Code CLI itself binds a random port per login), so the listener
|
|
6
|
-
* prefers the historical port 53692 and falls back to an ephemeral port when
|
|
7
|
-
* another login - in this process or in another one - still holds it. With a
|
|
8
|
-
* fixed port, a second login on the same machine sent its browser redirect to
|
|
9
|
-
* the OTHER process's listener, which could only answer "State mismatch".
|
|
10
|
-
*
|
|
11
|
-
* NOTE: Node-only (http.createServer); reached through the lazy OAuth loader.
|
|
12
|
-
*/
|
|
13
|
-
export type CallbackCode = {
|
|
14
|
-
code: string;
|
|
15
|
-
state: string;
|
|
16
|
-
};
|
|
17
|
-
export type CallbackListener = {
|
|
18
|
-
/** Bound loopback port; `undefined` when no port could be bound (manual-only login). */
|
|
19
|
-
port?: number;
|
|
20
|
-
redirectUri: string;
|
|
21
|
-
callbackUnavailable?: {
|
|
22
|
-
code: string;
|
|
23
|
-
};
|
|
24
|
-
waitForCode: () => Promise<CallbackCode | null>;
|
|
25
|
-
cancelWait: () => void;
|
|
26
|
-
close: () => Promise<void>;
|
|
27
|
-
};
|
|
28
|
-
export type CallbackListenerApis = {
|
|
29
|
-
createServer: typeof import("node:http").createServer;
|
|
30
|
-
};
|
|
31
|
-
export declare const PREFERRED_CALLBACK_PORT = 53692;
|
|
32
|
-
export declare const CALLBACK_PATH = "/callback";
|
|
33
|
-
export declare function callbackRedirectUri(port: number): string;
|
|
34
|
-
/**
|
|
35
|
-
* Binds the preferred port, then an ephemeral one; when neither can be bound
|
|
36
|
-
* the login continues in manual mode with the registered preferred-port
|
|
37
|
-
* redirect URI, so a pasted redirect URL still exchanges.
|
|
38
|
-
*/
|
|
39
|
-
export declare function startCallbackListener(apis: CallbackListenerApis, expectedState: string, host: string): Promise<CallbackListener>;
|
|
40
|
-
//# sourceMappingURL=anthropic-callback-listener.d.ts.map
|