@code-yeongyu/senpi-ai 2026.10.26-2 → 2026.10.27-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/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/providers/data/.manifest.json +1 -1
- package/dist/providers/data/azure-openai-responses.json +1 -1
- package/dist/providers/data/github-copilot.json +1 -1
- package/dist/providers/data/huggingface.json +1 -1
- package/dist/providers/data/moonshotai-cn.json +1 -1
- package/dist/providers/data/moonshotai.json +1 -1
- package/dist/providers/data/openai.json +1 -1
- package/dist/providers/data/opencode.json +1 -1
- package/dist/providers/data/opengateway.json +1 -1
- package/dist/providers/data/openrouter.json +1 -1
- package/dist/providers/data/radius.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 +12 -1
- package/dist/utils/assistant-capture-record.d.ts +13 -0
- package/dist/utils/assistant-capture-record.js +163 -0
- package/dist/utils/assistant-capture.d.ts +10 -4
- package/dist/utils/assistant-capture.js +2 -159
- package/dist/utils/assistant-seed-frontier.d.ts +7 -0
- package/dist/utils/assistant-seed-frontier.js +14 -0
- package/dist/utils/event-stream.d.ts +1 -0
- package/dist/utils/event-stream.js +13 -0
- package/dist/utils/provider-error.d.ts +12 -0
- package/dist/utils/provider-error.js +28 -0
- package/dist/utils/retry.js +3 -0
- package/dist/utils/secret-classifier.d.ts +28 -0
- package/dist/utils/secret-classifier.js +258 -0
- package/dist/utils/secret-lexer.d.ts +51 -0
- package/dist/utils/secret-lexer.js +295 -0
- package/dist/utils/secret-redaction.d.ts +13 -0
- package/dist/utils/secret-redaction.js +198 -0
- package/package.json +2 -2
package/dist/types.d.ts
CHANGED
|
@@ -569,10 +569,16 @@ export interface AssistantMessage {
|
|
|
569
569
|
stopDetails?: AssistantStopDetails;
|
|
570
570
|
deferred?: DeferredHandle;
|
|
571
571
|
errorMessage?: string;
|
|
572
|
-
/**
|
|
572
|
+
/**
|
|
573
|
+
* Redaction-safe cause: a provider limit projected as user-facing advice, or a model id its route does not
|
|
574
|
+
* serve (`model_config`, never retried or fallen back).
|
|
575
|
+
*/
|
|
573
576
|
errorCause?: {
|
|
574
577
|
type: "usage_limit";
|
|
575
578
|
scope: "account" | "model";
|
|
579
|
+
} | {
|
|
580
|
+
type: "model_config";
|
|
581
|
+
model: string;
|
|
576
582
|
};
|
|
577
583
|
/** Structured provider failure family, minted only by a provider adapter from structured error metadata. */
|
|
578
584
|
providerDiagnostic?: ProviderDiagnostic;
|
|
@@ -583,6 +589,11 @@ export interface AssistantMessage {
|
|
|
583
589
|
* No model produced the failure, so retry and model fallback must not act on it.
|
|
584
590
|
*/
|
|
585
591
|
failureSource?: "local";
|
|
592
|
+
/**
|
|
593
|
+
* Set by the model runtime on a failed response when the same credential (provider, endpoint and key) served a
|
|
594
|
+
* response moments before: an `auth` failure then means the provider refused this request, not the key.
|
|
595
|
+
*/
|
|
596
|
+
credentialServedRecently?: true;
|
|
586
597
|
rawStopReason?: string;
|
|
587
598
|
/**
|
|
588
599
|
* Provider indication of whether the model explicitly ended its turn.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AssistantMessage, AssistantMessageEvent } from "../types.ts";
|
|
2
|
+
import type { AssistantCapture, CaptureHint, CaptureState } from "./assistant-capture.ts";
|
|
3
|
+
type PublicBlock = AssistantMessage["content"][number];
|
|
4
|
+
export declare function freezeCapture<T>(value: T): T;
|
|
5
|
+
/** Only declared public fields survive; provider scratch and symbols never enter a seed. */
|
|
6
|
+
export declare function publicBlock(block: PublicBlock): PublicBlock;
|
|
7
|
+
/** Structural equality over plain data; any other object counts as changed, so a reused copy is never stale. */
|
|
8
|
+
export declare function sameValue(left: unknown, right: unknown): boolean;
|
|
9
|
+
/** A terminal replaced downstream: its usage and message, plus every scalar that differs from the source terminal. */
|
|
10
|
+
export declare function terminalCapture(capture: AssistantCapture, terminal: AssistantMessage): AssistantCapture;
|
|
11
|
+
export declare function buildCapture(state: CaptureState, event: AssistantMessageEvent, hint: CaptureHint): AssistantCapture | undefined;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=assistant-capture-record.d.ts.map
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { seedNextOffset } from "./assistant-seed-frontier.js";
|
|
2
|
+
const NO_CHANGE = Object.freeze({});
|
|
3
|
+
export function freezeCapture(value) {
|
|
4
|
+
const freeze = (part) => {
|
|
5
|
+
if (part === null || typeof part !== "object" || Object.isFrozen(part))
|
|
6
|
+
return;
|
|
7
|
+
for (const child of Object.values(part))
|
|
8
|
+
freeze(child);
|
|
9
|
+
Object.freeze(part);
|
|
10
|
+
};
|
|
11
|
+
const copy = structuredClone(value);
|
|
12
|
+
freeze(copy);
|
|
13
|
+
return copy;
|
|
14
|
+
}
|
|
15
|
+
/** Only declared public fields survive; provider scratch and symbols never enter a seed. */
|
|
16
|
+
export function publicBlock(block) {
|
|
17
|
+
const fields = {
|
|
18
|
+
text: ["type", "text", "audience", "textSignature"],
|
|
19
|
+
thinking: ["type", "thinking", "thinkingSignature", "redacted", "startedAt", "endedAt"],
|
|
20
|
+
toolCall: ["type", "id", "name", "arguments", "incomplete", "errorMessage", "thoughtSignature", "namespace"],
|
|
21
|
+
providerNative: ["type", "subtype", "raw"],
|
|
22
|
+
};
|
|
23
|
+
return Object.fromEntries(fields[block.type].filter((key) => key in block).map((key) => [key, Reflect.get(block, key)]));
|
|
24
|
+
}
|
|
25
|
+
/** Structural equality over plain data; any other object counts as changed, so a reused copy is never stale. */
|
|
26
|
+
export function sameValue(left, right) {
|
|
27
|
+
if (left === right)
|
|
28
|
+
return true;
|
|
29
|
+
if (!isPlainData(left) || !isPlainData(right) || Array.isArray(left) !== Array.isArray(right))
|
|
30
|
+
return false;
|
|
31
|
+
const keys = Object.keys(left);
|
|
32
|
+
return (keys.length === Object.keys(right).length &&
|
|
33
|
+
keys.every((key) => Object.hasOwn(right, key) && sameValue(Reflect.get(left, key), Reflect.get(right, key))));
|
|
34
|
+
}
|
|
35
|
+
function isPlainData(value) {
|
|
36
|
+
if (typeof value !== "object" || value === null)
|
|
37
|
+
return false;
|
|
38
|
+
const prototype = Object.getPrototypeOf(value);
|
|
39
|
+
return prototype === Object.prototype || prototype === Array.prototype || prototype === null;
|
|
40
|
+
}
|
|
41
|
+
/** A terminal replaced downstream: its usage and message, plus every scalar that differs from the source terminal. */
|
|
42
|
+
export function terminalCapture(capture, terminal) {
|
|
43
|
+
const metadata = { ...capture.metadata };
|
|
44
|
+
const previous = capture.message ?? {};
|
|
45
|
+
for (const key of new Set([...Object.keys(previous), ...Object.keys(terminal)])) {
|
|
46
|
+
if (key === "content" || key === "usage")
|
|
47
|
+
continue;
|
|
48
|
+
const value = Reflect.get(terminal, key);
|
|
49
|
+
if (!sameValue(value, Reflect.get(previous, key)))
|
|
50
|
+
metadata[key] = key in terminal ? value : null;
|
|
51
|
+
}
|
|
52
|
+
return Object.freeze({
|
|
53
|
+
usage: frozenUsage(terminal.usage),
|
|
54
|
+
metadata: freezeCapture(metadata),
|
|
55
|
+
message: freezeCapture({ ...terminal, content: terminal.content.map(publicBlock) }),
|
|
56
|
+
...(capture.rewrittenFrom === undefined ? {} : { rewrittenFrom: capture.rewrittenFrom }),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export function buildCapture(state, event, hint) {
|
|
60
|
+
const message = event.type === "done" ? event.message : event.type === "error" ? event.error : event.partial;
|
|
61
|
+
const usage = usageCopy(state, message.usage);
|
|
62
|
+
if (!("contentIndex" in event)) {
|
|
63
|
+
return Object.freeze({
|
|
64
|
+
usage,
|
|
65
|
+
metadata: changedMetadata(state, message),
|
|
66
|
+
message: freezeCapture({ ...message, content: message.content.map(publicBlock) }),
|
|
67
|
+
...(hint.rewrittenFrom === undefined ? {} : { rewrittenFrom: hint.rewrittenFrom }),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const index = hint.contentIndex ?? event.contentIndex;
|
|
71
|
+
const own = index === event.contentIndex ? {} : { contentIndex: index };
|
|
72
|
+
const carried = hint.carried;
|
|
73
|
+
if ("delta" in event) {
|
|
74
|
+
if (carried) {
|
|
75
|
+
const { parsedThrough } = carried;
|
|
76
|
+
return Object.freeze({
|
|
77
|
+
usage,
|
|
78
|
+
offset: carried.offset,
|
|
79
|
+
...(parsedThrough === undefined ? {} : { parsedThrough }),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const position = state.positions.get(index) ?? { offset: 0, parsedThrough: 0 };
|
|
83
|
+
const offset = hint.offset ?? position.offset;
|
|
84
|
+
position.offset = offset + event.delta.length;
|
|
85
|
+
if (event.type === "toolcall_delta" && event.delta.length > 0) {
|
|
86
|
+
position.parsedThrough = hint.parsedThrough ?? Math.max(position.parsedThrough, position.offset);
|
|
87
|
+
}
|
|
88
|
+
state.positions.set(index, position);
|
|
89
|
+
return Object.freeze({
|
|
90
|
+
usage,
|
|
91
|
+
offset,
|
|
92
|
+
...own,
|
|
93
|
+
...(event.type === "toolcall_delta" ? { parsedThrough: position.parsedThrough } : {}),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const metadata = changedMetadata(state, message);
|
|
97
|
+
if (carried) {
|
|
98
|
+
const { usage: _usage, metadata: _metadata, message: _message, offset: _offset, ...facts } = carried;
|
|
99
|
+
return Object.freeze({ ...facts, usage, metadata });
|
|
100
|
+
}
|
|
101
|
+
const starts = event.type === "text_start" || event.type === "thinking_start" || event.type === "toolcall_start";
|
|
102
|
+
const source = event.type === "toolcall_end" ? event.toolCall : message.content[index];
|
|
103
|
+
// A start announced before its block is appended has no seed to freeze yet.
|
|
104
|
+
if (starts && source === undefined)
|
|
105
|
+
return undefined;
|
|
106
|
+
const block = freezeCapture(publicBlock(source));
|
|
107
|
+
if (!starts)
|
|
108
|
+
return Object.freeze({ usage, metadata, block, ...own });
|
|
109
|
+
const rawArgumentPrefix = hint.rawArgumentPrefix ?? "";
|
|
110
|
+
const coveredCodeUnits = block.type === "text"
|
|
111
|
+
? block.text.length
|
|
112
|
+
: block.type === "thinking"
|
|
113
|
+
? block.thinking.length
|
|
114
|
+
: rawArgumentPrefix.length;
|
|
115
|
+
const parsedThrough = hint.parsedThrough ?? 0;
|
|
116
|
+
const nextOffset = seedNextOffset(hint.offset ?? coveredCodeUnits, coveredCodeUnits);
|
|
117
|
+
state.positions.set(index, { offset: nextOffset, parsedThrough });
|
|
118
|
+
return Object.freeze({
|
|
119
|
+
usage,
|
|
120
|
+
metadata,
|
|
121
|
+
block,
|
|
122
|
+
coveredCodeUnits,
|
|
123
|
+
nextOffset,
|
|
124
|
+
...own,
|
|
125
|
+
...(block.type === "toolCall" ? { rawArgumentPrefix, parsedThrough } : {}),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function usageCopy(state, usage) {
|
|
129
|
+
if (state.usage !== undefined && sameValue(usage, state.usage))
|
|
130
|
+
return state.usage;
|
|
131
|
+
state.usage = frozenUsage(usage);
|
|
132
|
+
return state.usage;
|
|
133
|
+
}
|
|
134
|
+
function frozenUsage(usage) {
|
|
135
|
+
return Object.freeze({
|
|
136
|
+
...usage,
|
|
137
|
+
cost: Object.freeze({ ...usage.cost }),
|
|
138
|
+
...(usage.contextUsage ? { contextUsage: Object.freeze({ ...usage.contextUsage }) } : {}),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/** Scalar message fields changed since the last non-delta capture; the baseline moves only once its copy exists. */
|
|
142
|
+
function changedMetadata(state, message) {
|
|
143
|
+
const changed = {};
|
|
144
|
+
for (const key of Object.keys(message)) {
|
|
145
|
+
if (key === "content" || key === "usage")
|
|
146
|
+
continue;
|
|
147
|
+
const value = Reflect.get(message, key);
|
|
148
|
+
if (!state.metadata.has(key) || state.metadata.get(key) !== value)
|
|
149
|
+
changed[key] = value;
|
|
150
|
+
}
|
|
151
|
+
for (const key of state.metadata.keys())
|
|
152
|
+
if (!(key in message))
|
|
153
|
+
changed[key] = null;
|
|
154
|
+
const frozen = Object.keys(changed).length === 0 ? NO_CHANGE : freezeCapture(changed);
|
|
155
|
+
for (const key of Object.keys(changed)) {
|
|
156
|
+
if (key in message)
|
|
157
|
+
state.metadata.set(key, Reflect.get(message, key));
|
|
158
|
+
else
|
|
159
|
+
state.metadata.delete(key);
|
|
160
|
+
}
|
|
161
|
+
return frozen;
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=assistant-capture-record.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AssistantMessage, AssistantMessageEvent, Usage } from "../types.ts";
|
|
2
|
+
export { freezeCapture, publicBlock } from "./assistant-capture-record.ts";
|
|
2
3
|
type PublicBlock = AssistantMessage["content"][number];
|
|
3
4
|
export type AssistantProgress = {
|
|
4
5
|
type: "message_block_state";
|
|
@@ -39,6 +40,8 @@ export interface AssistantCapture {
|
|
|
39
40
|
readonly message?: AssistantMessage;
|
|
40
41
|
readonly rawArgumentPrefix?: string;
|
|
41
42
|
readonly coveredCodeUnits?: number;
|
|
43
|
+
/** Required on block starts: zero for replay, full seed length for continuation. */
|
|
44
|
+
readonly nextOffset?: number;
|
|
42
45
|
readonly offset?: number;
|
|
43
46
|
readonly parsedThrough?: number;
|
|
44
47
|
/** The block's own index where the event's public `contentIndex` names another block; internal readers prefer it. */
|
|
@@ -51,6 +54,13 @@ export interface AssistantCapture {
|
|
|
51
54
|
*/
|
|
52
55
|
readonly rewrittenFrom?: number;
|
|
53
56
|
}
|
|
57
|
+
export interface CaptureState {
|
|
58
|
+
positions: Map<number, CapturePosition>;
|
|
59
|
+
metadata: Map<string, unknown>;
|
|
60
|
+
usage?: Usage;
|
|
61
|
+
insertCopies: Map<number, PublicBlock>;
|
|
62
|
+
failedBlocks: Set<number>;
|
|
63
|
+
}
|
|
54
64
|
/** While held, every capture-enabled stream whose first push comes after it builds capture records. */
|
|
55
65
|
export declare function subscribeAssistantCapture(): () => void;
|
|
56
66
|
export declare function setAssistantReader(owner: object, reader: QueueReader): void;
|
|
@@ -90,12 +100,8 @@ export interface CapturePosition {
|
|
|
90
100
|
offset: number;
|
|
91
101
|
parsedThrough: number;
|
|
92
102
|
}
|
|
93
|
-
export declare function freezeCapture<T>(value: T): T;
|
|
94
|
-
/** Only declared public fields survive; provider scratch and symbols never enter a seed. */
|
|
95
|
-
export declare function publicBlock(block: PublicBlock): PublicBlock;
|
|
96
103
|
export declare function dropAssistantCapture(event: object): void;
|
|
97
104
|
export declare function carryAssistantCapture<T extends object>(source: object, target: T, terminal?: AssistantMessage): T;
|
|
98
105
|
/** Never parses or traverses a block on a delta, including a no-growth delta. */
|
|
99
106
|
export declare function captureAssistantEvent(owner: object, event: AssistantMessageEvent, hint?: CaptureHint): void;
|
|
100
|
-
export {};
|
|
101
107
|
//# sourceMappingURL=assistant-capture.d.ts.map
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { buildCapture, freezeCapture, publicBlock, sameValue, terminalCapture } from "./assistant-capture-record.js";
|
|
2
|
+
export { freezeCapture, publicBlock } from "./assistant-capture-record.js";
|
|
1
3
|
const captures = new WeakMap();
|
|
2
4
|
const readers = new WeakMap();
|
|
3
5
|
const writers = new WeakMap();
|
|
@@ -5,7 +7,6 @@ const writers = new WeakMap();
|
|
|
5
7
|
const states = new WeakMap();
|
|
6
8
|
const demanded = new WeakSet();
|
|
7
9
|
const warned = new WeakSet();
|
|
8
|
-
const NO_CHANGE = Object.freeze({});
|
|
9
10
|
let subscribers = 0;
|
|
10
11
|
let capturedAny = false;
|
|
11
12
|
/** While held, every capture-enabled stream whose first push comes after it builds capture records. */
|
|
@@ -132,44 +133,6 @@ export function isAssistantProgress(event) {
|
|
|
132
133
|
event.type === "message_block_metadata" ||
|
|
133
134
|
event.type === "message_tool_arguments_reset");
|
|
134
135
|
}
|
|
135
|
-
export function freezeCapture(value) {
|
|
136
|
-
const freeze = (part) => {
|
|
137
|
-
if (part === null || typeof part !== "object" || Object.isFrozen(part))
|
|
138
|
-
return;
|
|
139
|
-
for (const child of Object.values(part))
|
|
140
|
-
freeze(child);
|
|
141
|
-
Object.freeze(part);
|
|
142
|
-
};
|
|
143
|
-
const copy = structuredClone(value);
|
|
144
|
-
freeze(copy);
|
|
145
|
-
return copy;
|
|
146
|
-
}
|
|
147
|
-
/** Only declared public fields survive; provider scratch and symbols never enter a seed. */
|
|
148
|
-
export function publicBlock(block) {
|
|
149
|
-
const fields = {
|
|
150
|
-
text: ["type", "text", "audience", "textSignature"],
|
|
151
|
-
thinking: ["type", "thinking", "thinkingSignature", "redacted", "startedAt", "endedAt"],
|
|
152
|
-
toolCall: ["type", "id", "name", "arguments", "incomplete", "errorMessage", "thoughtSignature", "namespace"],
|
|
153
|
-
providerNative: ["type", "subtype", "raw"],
|
|
154
|
-
};
|
|
155
|
-
return Object.fromEntries(fields[block.type].filter((key) => key in block).map((key) => [key, Reflect.get(block, key)]));
|
|
156
|
-
}
|
|
157
|
-
/** Structural equality over plain data; any other object counts as changed, so a reused copy is never stale. */
|
|
158
|
-
function sameValue(left, right) {
|
|
159
|
-
if (left === right)
|
|
160
|
-
return true;
|
|
161
|
-
if (!isPlainData(left) || !isPlainData(right) || Array.isArray(left) !== Array.isArray(right))
|
|
162
|
-
return false;
|
|
163
|
-
const keys = Object.keys(left);
|
|
164
|
-
return (keys.length === Object.keys(right).length &&
|
|
165
|
-
keys.every((key) => Object.hasOwn(right, key) && sameValue(Reflect.get(left, key), Reflect.get(right, key))));
|
|
166
|
-
}
|
|
167
|
-
function isPlainData(value) {
|
|
168
|
-
if (typeof value !== "object" || value === null)
|
|
169
|
-
return false;
|
|
170
|
-
const prototype = Object.getPrototypeOf(value);
|
|
171
|
-
return prototype === Object.prototype || prototype === Array.prototype || prototype === null;
|
|
172
|
-
}
|
|
173
136
|
export function dropAssistantCapture(event) {
|
|
174
137
|
if (capturedAny)
|
|
175
138
|
captures.delete(event);
|
|
@@ -186,24 +149,6 @@ export function carryAssistantCapture(source, target, terminal) {
|
|
|
186
149
|
}
|
|
187
150
|
return target;
|
|
188
151
|
}
|
|
189
|
-
/** A terminal replaced downstream: its usage and message, plus every scalar that differs from the source terminal. */
|
|
190
|
-
function terminalCapture(capture, terminal) {
|
|
191
|
-
const metadata = { ...capture.metadata };
|
|
192
|
-
const previous = capture.message ?? {};
|
|
193
|
-
for (const key of new Set([...Object.keys(previous), ...Object.keys(terminal)])) {
|
|
194
|
-
if (key === "content" || key === "usage")
|
|
195
|
-
continue;
|
|
196
|
-
const value = Reflect.get(terminal, key);
|
|
197
|
-
if (!sameValue(value, Reflect.get(previous, key)))
|
|
198
|
-
metadata[key] = key in terminal ? value : null;
|
|
199
|
-
}
|
|
200
|
-
return Object.freeze({
|
|
201
|
-
usage: frozenUsage(terminal.usage),
|
|
202
|
-
metadata: freezeCapture(metadata),
|
|
203
|
-
message: freezeCapture({ ...terminal, content: terminal.content.map(publicBlock) }),
|
|
204
|
-
...(capture.rewrittenFrom === undefined ? {} : { rewrittenFrom: capture.rewrittenFrom }),
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
152
|
/** Never parses or traverses a block on a delta, including a no-growth delta. */
|
|
208
153
|
export function captureAssistantEvent(owner, event, hint = {}) {
|
|
209
154
|
const state = activeState(owner);
|
|
@@ -222,106 +167,4 @@ export function captureAssistantEvent(owner, event, hint = {}) {
|
|
|
222
167
|
dropCapture(owner, error, state, contentIndex);
|
|
223
168
|
}
|
|
224
169
|
}
|
|
225
|
-
function buildCapture(state, event, hint) {
|
|
226
|
-
const message = event.type === "done" ? event.message : event.type === "error" ? event.error : event.partial;
|
|
227
|
-
const usage = usageCopy(state, message.usage);
|
|
228
|
-
if (!("contentIndex" in event)) {
|
|
229
|
-
return Object.freeze({
|
|
230
|
-
usage,
|
|
231
|
-
metadata: changedMetadata(state, message),
|
|
232
|
-
message: freezeCapture({ ...message, content: message.content.map(publicBlock) }),
|
|
233
|
-
...(hint.rewrittenFrom === undefined ? {} : { rewrittenFrom: hint.rewrittenFrom }),
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
const index = hint.contentIndex ?? event.contentIndex;
|
|
237
|
-
const own = index === event.contentIndex ? {} : { contentIndex: index };
|
|
238
|
-
const carried = hint.carried;
|
|
239
|
-
if ("delta" in event) {
|
|
240
|
-
if (carried) {
|
|
241
|
-
const { parsedThrough } = carried;
|
|
242
|
-
return Object.freeze({
|
|
243
|
-
usage,
|
|
244
|
-
offset: carried.offset,
|
|
245
|
-
...(parsedThrough === undefined ? {} : { parsedThrough }),
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
const position = state.positions.get(index) ?? { offset: 0, parsedThrough: 0 };
|
|
249
|
-
const offset = hint.offset ?? position.offset;
|
|
250
|
-
position.offset = offset + event.delta.length;
|
|
251
|
-
if (event.type === "toolcall_delta" && event.delta.length > 0) {
|
|
252
|
-
position.parsedThrough = hint.parsedThrough ?? Math.max(position.parsedThrough, position.offset);
|
|
253
|
-
}
|
|
254
|
-
state.positions.set(index, position);
|
|
255
|
-
return Object.freeze({
|
|
256
|
-
usage,
|
|
257
|
-
offset,
|
|
258
|
-
...own,
|
|
259
|
-
...(event.type === "toolcall_delta" ? { parsedThrough: position.parsedThrough } : {}),
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
const metadata = changedMetadata(state, message);
|
|
263
|
-
if (carried) {
|
|
264
|
-
const { usage: _usage, metadata: _metadata, message: _message, offset: _offset, ...facts } = carried;
|
|
265
|
-
return Object.freeze({ ...facts, usage, metadata });
|
|
266
|
-
}
|
|
267
|
-
const starts = event.type === "text_start" || event.type === "thinking_start" || event.type === "toolcall_start";
|
|
268
|
-
const source = event.type === "toolcall_end" ? event.toolCall : message.content[index];
|
|
269
|
-
// A start announced before its block is appended has no seed to freeze yet.
|
|
270
|
-
if (starts && source === undefined)
|
|
271
|
-
return undefined;
|
|
272
|
-
const block = freezeCapture(publicBlock(source));
|
|
273
|
-
if (!starts)
|
|
274
|
-
return Object.freeze({ usage, metadata, block, ...own });
|
|
275
|
-
const rawArgumentPrefix = hint.rawArgumentPrefix ?? "";
|
|
276
|
-
const coveredCodeUnits = block.type === "text"
|
|
277
|
-
? block.text.length
|
|
278
|
-
: block.type === "thinking"
|
|
279
|
-
? block.thinking.length
|
|
280
|
-
: rawArgumentPrefix.length;
|
|
281
|
-
const parsedThrough = hint.parsedThrough ?? 0;
|
|
282
|
-
state.positions.set(index, { offset: hint.offset ?? coveredCodeUnits, parsedThrough });
|
|
283
|
-
return Object.freeze({
|
|
284
|
-
usage,
|
|
285
|
-
metadata,
|
|
286
|
-
block,
|
|
287
|
-
coveredCodeUnits,
|
|
288
|
-
...own,
|
|
289
|
-
...(block.type === "toolCall" ? { rawArgumentPrefix, parsedThrough } : {}),
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
function usageCopy(state, usage) {
|
|
293
|
-
if (state.usage !== undefined && sameValue(usage, state.usage))
|
|
294
|
-
return state.usage;
|
|
295
|
-
state.usage = frozenUsage(usage);
|
|
296
|
-
return state.usage;
|
|
297
|
-
}
|
|
298
|
-
function frozenUsage(usage) {
|
|
299
|
-
return Object.freeze({
|
|
300
|
-
...usage,
|
|
301
|
-
cost: Object.freeze({ ...usage.cost }),
|
|
302
|
-
...(usage.contextUsage ? { contextUsage: Object.freeze({ ...usage.contextUsage }) } : {}),
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
/** Scalar message fields changed since the last non-delta capture; the baseline moves only once its copy exists. */
|
|
306
|
-
function changedMetadata(state, message) {
|
|
307
|
-
const changed = {};
|
|
308
|
-
for (const key of Object.keys(message)) {
|
|
309
|
-
if (key === "content" || key === "usage")
|
|
310
|
-
continue;
|
|
311
|
-
const value = Reflect.get(message, key);
|
|
312
|
-
if (!state.metadata.has(key) || state.metadata.get(key) !== value)
|
|
313
|
-
changed[key] = value;
|
|
314
|
-
}
|
|
315
|
-
for (const key of state.metadata.keys())
|
|
316
|
-
if (!(key in message))
|
|
317
|
-
changed[key] = null;
|
|
318
|
-
const frozen = Object.keys(changed).length === 0 ? NO_CHANGE : freezeCapture(changed);
|
|
319
|
-
for (const key of Object.keys(changed)) {
|
|
320
|
-
if (key in message)
|
|
321
|
-
state.metadata.set(key, Reflect.get(message, key));
|
|
322
|
-
else
|
|
323
|
-
state.metadata.delete(key);
|
|
324
|
-
}
|
|
325
|
-
return frozen;
|
|
326
|
-
}
|
|
327
170
|
//# sourceMappingURL=assistant-capture.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare class SeedFrontierError extends Error {
|
|
2
|
+
readonly code: "missing_next_offset" | "invalid_next_offset";
|
|
3
|
+
constructor(nextOffset: number | undefined);
|
|
4
|
+
}
|
|
5
|
+
/** No inference at a consumer: only full replay or full continuation is supported. */
|
|
6
|
+
export declare function seedNextOffset(nextOffset: number | undefined, seedLength: number): number;
|
|
7
|
+
//# sourceMappingURL=assistant-seed-frontier.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class SeedFrontierError extends Error {
|
|
2
|
+
constructor(nextOffset) {
|
|
3
|
+
super("Block seed requires nextOffset equal to zero or its full UTF-16 length");
|
|
4
|
+
this.name = "SeedFrontierError";
|
|
5
|
+
this.code = nextOffset === undefined ? "missing_next_offset" : "invalid_next_offset";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/** No inference at a consumer: only full replay or full continuation is supported. */
|
|
9
|
+
export function seedNextOffset(nextOffset, seedLength) {
|
|
10
|
+
if (nextOffset === undefined || !Number.isSafeInteger(nextOffset) || (nextOffset !== 0 && nextOffset !== seedLength))
|
|
11
|
+
throw new SeedFrontierError(nextOffset);
|
|
12
|
+
return nextOffset;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=assistant-seed-frontier.js.map
|
|
@@ -40,6 +40,7 @@ export declare class AssistantMessageEventStream extends EventStream<AssistantMe
|
|
|
40
40
|
});
|
|
41
41
|
push(event: AssistantMessageEvent, hint?: CaptureHint): void;
|
|
42
42
|
[Symbol.asyncIterator](): AsyncIterator<AssistantMessageEvent>;
|
|
43
|
+
end(result?: AssistantMessage): void;
|
|
43
44
|
}
|
|
44
45
|
/** Factory function for AssistantMessageEventStream (for use in extensions) */
|
|
45
46
|
export declare function createAssistantMessageEventStream(): AssistantMessageEventStream;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { captureAssistantEvent, dropAssistantCapture, enableAssistantCapture, setAssistantProgressWriter, setAssistantReader, } from "./assistant-capture.js";
|
|
2
|
+
import { sanitizeProviderErrorMessage } from "./provider-error.js";
|
|
2
3
|
class FifoQueue {
|
|
3
4
|
constructor() {
|
|
4
5
|
this.incoming = [];
|
|
@@ -188,6 +189,13 @@ export class AssistantMessageEventStream extends EventStream {
|
|
|
188
189
|
push(event, hint) {
|
|
189
190
|
if (this.done)
|
|
190
191
|
return;
|
|
192
|
+
// Classify raw provider errors and redact before capture or publication (senpi#4282).
|
|
193
|
+
if ("partial" in event)
|
|
194
|
+
sanitizeProviderErrorMessage(event.partial);
|
|
195
|
+
if (event.type === "error")
|
|
196
|
+
sanitizeProviderErrorMessage(event.error);
|
|
197
|
+
if (event.type === "done")
|
|
198
|
+
sanitizeProviderErrorMessage(event.message);
|
|
191
199
|
captureAssistantEvent(this, event, hint);
|
|
192
200
|
super.push(event);
|
|
193
201
|
}
|
|
@@ -200,6 +208,11 @@ export class AssistantMessageEventStream extends EventStream {
|
|
|
200
208
|
return { value: entry.event, done: false };
|
|
201
209
|
});
|
|
202
210
|
}
|
|
211
|
+
end(result) {
|
|
212
|
+
if (result !== undefined)
|
|
213
|
+
sanitizeProviderErrorMessage(result);
|
|
214
|
+
super.end(result);
|
|
215
|
+
}
|
|
203
216
|
}
|
|
204
217
|
/** Factory function for AssistantMessageEventStream (for use in extensions) */
|
|
205
218
|
export function createAssistantMessageEventStream() {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AssistantMessage } from "../types.ts";
|
|
2
|
+
import { redactCredentialShapes, redactSecrets } from "./secret-redaction.ts";
|
|
3
|
+
export { redactCredentialShapes, redactSecrets };
|
|
4
|
+
/** True when provider text says the route does not serve the configured model id (senpi#4282). */
|
|
5
|
+
export declare function isModelConfigError(errorMessage: string | undefined): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* The single point where a provider error body enters a message: classify a model configuration error on the RAW
|
|
8
|
+
* text first (carried as `errorCause`), then keep only the redacted text for every provider error. Idempotent;
|
|
9
|
+
* called for every message a provider stream publishes, so no partial, event or final result carries a raw body.
|
|
10
|
+
*/
|
|
11
|
+
export declare function sanitizeProviderErrorMessage(message: AssistantMessage): void;
|
|
12
|
+
//# sourceMappingURL=provider-error.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { redactCredentialShapes, redactSecrets } from "./secret-redaction.js";
|
|
2
|
+
export { redactCredentialShapes, redactSecrets };
|
|
3
|
+
// A route that does not serve a configured model id: the OpenAI-compatible `model_not_found` code and OpenAI's
|
|
4
|
+
// "The model `<id>` does not exist" prose, a gateway's "unknown provider for model", Anthropic's not_found_error whose
|
|
5
|
+
// message names the model ("model: <id>"; any other not_found_error is not one), and Google's "models/<id> is not found".
|
|
6
|
+
// Every quantifier is bounded, so a long run never makes a match attempt expensive.
|
|
7
|
+
const MODEL_CONFIG_ERROR = /\bmodel_not_found\b|\bunknown provider for model\b|\bthe model\s{1,8}[`'"]?[\w.:/-]{1,256}[`'"]?\s{1,8}does not exist\b|\bnot_found_error\b[^}]{0,120}?\bmodel:\s{0,8}[\w.:/-]|\bmodels\/[\w.:-]{1,256} is not found\b/i;
|
|
8
|
+
/** True when provider text says the route does not serve the configured model id (senpi#4282). */
|
|
9
|
+
export function isModelConfigError(errorMessage) {
|
|
10
|
+
return errorMessage !== undefined && MODEL_CONFIG_ERROR.test(errorMessage);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The single point where a provider error body enters a message: classify a model configuration error on the RAW
|
|
14
|
+
* text first (carried as `errorCause`), then keep only the redacted text for every provider error. Idempotent;
|
|
15
|
+
* called for every message a provider stream publishes, so no partial, event or final result carries a raw body.
|
|
16
|
+
*/
|
|
17
|
+
export function sanitizeProviderErrorMessage(message) {
|
|
18
|
+
const raw = message.errorMessage;
|
|
19
|
+
if (raw === undefined || raw.length === 0)
|
|
20
|
+
return;
|
|
21
|
+
if (message.stopReason === "error" && message.errorCause?.type !== "model_config" && isModelConfigError(raw)) {
|
|
22
|
+
message.errorCause = { type: "model_config", model: `${message.provider}/${message.model}` };
|
|
23
|
+
}
|
|
24
|
+
const redacted = redactSecrets(raw);
|
|
25
|
+
if (redacted !== raw)
|
|
26
|
+
message.errorMessage = redacted;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=provider-error.js.map
|
package/dist/utils/retry.js
CHANGED
|
@@ -389,6 +389,9 @@ export async function retryAssistantCall(produce, policy, signal, callbacks) {
|
|
|
389
389
|
* before restarting the assistant turn.
|
|
390
390
|
*/
|
|
391
391
|
export function isRetryableAssistantError(message) {
|
|
392
|
+
// A route that does not serve the model answers the same on every attempt, whatever status wraps it.
|
|
393
|
+
if (message.errorCause?.type === "model_config")
|
|
394
|
+
return false;
|
|
392
395
|
if (message.stopReason === "error" &&
|
|
393
396
|
message.diagnostics?.some((diagnostic) => diagnostic.type === OAUTH_REFRESH_UNAVAILABLE_DIAGNOSTIC)) {
|
|
394
397
|
return true;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Names whose whole value is a credential at any length; every other credential name needs a 16-character value. */
|
|
2
|
+
export declare const STRICT_WORDS: Set<string>;
|
|
3
|
+
export declare const COOKIE_WORDS: Set<string>;
|
|
4
|
+
export declare const SCHEMES: Set<string>;
|
|
5
|
+
export declare const COOKIE_ATTRIBUTES: Set<string>;
|
|
6
|
+
/**
|
|
7
|
+
* The credential word a name ends with, after its normalization: numbered slots (OPENAI_API_KEY_2, token2,
|
|
8
|
+
* x-api-key-1) and a trailing qualifier (api_key_value, apiKeyId) still end with the word. "session" counts only as
|
|
9
|
+
* the whole name, so session_id stays readable; a word elsewhere in a name (token_type, primary_key_column) or inside
|
|
10
|
+
* a word (monkey, tokenizer, max_tokens) does not count.
|
|
11
|
+
*/
|
|
12
|
+
export declare function secretWord(text: string, start: number, end: number): string | undefined;
|
|
13
|
+
/** Where the name ends a token: its last run of name characters (a URL path or escape before it is not the name). */
|
|
14
|
+
export declare function nameStart(text: string, start: number, end: number): number;
|
|
15
|
+
export declare function lowerToken(text: string, start: number, end: number, maxLength: number): string | undefined;
|
|
16
|
+
export declare function isInert(text: string, start: number, end: number): boolean;
|
|
17
|
+
/** A known key ends at its alphabet's boundary, not at the end of a URL or filename containing it. */
|
|
18
|
+
export declare function credentialShapeEnd(text: string, start: number, end: number): number;
|
|
19
|
+
/**
|
|
20
|
+
* Where the first credential-shaped run in [start, end) begins, or -1: a known key prefix or a JWT at the start of a
|
|
21
|
+
* word anywhere in the token (after "/", "+", "~", "-", "." or "%", so URL userinfo, paths and joined values count),
|
|
22
|
+
* including percent-encoded token punctuation, never inside a word ("tasks-..." is not "sk-...").
|
|
23
|
+
* After a credential has already been found, withinCredential also checks inside words: an adjoining key may have
|
|
24
|
+
* had its prefix consumed by the first key's alphabet. The caller retains the whole token in that ambiguous case.
|
|
25
|
+
* Constant work per character plus the JWT walks bounded above.
|
|
26
|
+
*/
|
|
27
|
+
export declare function credentialShapeStart(text: string, start: number, end: number, withinCredential?: boolean): number;
|
|
28
|
+
//# sourceMappingURL=secret-classifier.d.ts.map
|