agent-accelerator 0.3.0 → 0.4.5
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/package.json +1 -1
- package/src/agent/agent.ts +845 -116
- package/src/agent/context.ts +35 -0
- package/src/agent/delegation.ts +390 -36
- package/src/agent/loop.ts +785 -110
- package/src/agent/subagent.ts +59 -0
- package/src/index.ts +173 -59
- package/src/models/catalog-cache.ts +68 -25
- package/src/models/catalog.ts +123 -18
- package/src/providers/google.ts +152 -43
- package/src/providers/openai-compat.ts +246 -96
- package/src/providers/openai.ts +274 -79
- package/src/providers/openrouter.ts +191 -82
- package/src/providers/registry.ts +16 -12
- package/src/providers/shared.ts +190 -0
- package/src/providers.ts +259 -9
- package/src/session/store.ts +521 -33
- package/src/streaming/event-stream.ts +156 -41
- package/src/tools/executor.ts +248 -15
- package/src/tools/tool.ts +147 -3
- package/src/types/agent.ts +244 -2
- package/src/types/errors.ts +133 -0
- package/src/types/model.ts +22 -1
- package/src/types/response.ts +133 -3
- package/src/types/tool.ts +69 -0
- package/src/update-models.ts +8 -2
- package/src/utils/cache.ts +68 -1
- package/src/utils/env.ts +23 -13
- package/src/utils/errors.ts +15 -1
- package/src/utils/headers.ts +153 -19
- package/src/utils/retry.ts +27 -13
- package/src/utils/session.ts +284 -0
- package/src/providers/openrouter-responses.ts +0 -949
- package/src/types/provider-payloads.ts +0 -352
package/src/types/response.ts
CHANGED
|
@@ -43,9 +43,66 @@ export interface SubAgentExecutionMetadata {
|
|
|
43
43
|
steps?: SubAgentStep[];
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/** Canonical finish reasons surfaced on AgentResponse (provider-agnostic).
|
|
47
|
+
* Vendors use varied strings (`end_turn`, `stop_sequence`, `max_tokens`,
|
|
48
|
+
* `tool_use`, ...); adapters map them to these values and preserve the
|
|
49
|
+
* original in `rawFinishReason`. Unknown non-empty reasons map to `"error"`.
|
|
50
|
+
* The `(string & {})` extension keeps vendor passthrough assignable at
|
|
51
|
+
* provider call sites that forward raw strings (normalize with
|
|
52
|
+
* `normalizeFinishReason` to canonicalize). */
|
|
53
|
+
export type FinishReason =
|
|
54
|
+
| "stop"
|
|
55
|
+
| "tool_calls"
|
|
56
|
+
| "length"
|
|
57
|
+
| "content_filter"
|
|
58
|
+
| "refusal"
|
|
59
|
+
| "max_turns"
|
|
60
|
+
| "error"
|
|
61
|
+
| (string & {});
|
|
62
|
+
|
|
63
|
+
/** Maps a vendor finish reason to canonical FinishReason + raw passthrough.
|
|
64
|
+
* Known aliases collapse to the canonical set; empty/undefined stays
|
|
65
|
+
* undefined; any other non-empty string maps to `"error"` with the original
|
|
66
|
+
* preserved in `rawFinishReason` (Q-04). */
|
|
67
|
+
export function normalizeFinishReason(raw?: string): {
|
|
68
|
+
finishReason?: FinishReason;
|
|
69
|
+
rawFinishReason?: string;
|
|
70
|
+
} {
|
|
71
|
+
if (raw === undefined || raw === null || raw === "") return {};
|
|
72
|
+
const r = String(raw);
|
|
73
|
+
const lower = r.toLowerCase();
|
|
74
|
+
const map: Record<string, FinishReason> = {
|
|
75
|
+
stop: "stop",
|
|
76
|
+
end_turn: "stop",
|
|
77
|
+
stop_sequence: "stop",
|
|
78
|
+
completed: "stop",
|
|
79
|
+
complete: "stop",
|
|
80
|
+
done: "stop",
|
|
81
|
+
tool_calls: "tool_calls",
|
|
82
|
+
tool_use: "tool_calls",
|
|
83
|
+
function_call: "tool_calls",
|
|
84
|
+
tool_call: "tool_calls",
|
|
85
|
+
length: "length",
|
|
86
|
+
max_tokens: "length",
|
|
87
|
+
max_turns: "max_turns",
|
|
88
|
+
content_filter: "content_filter",
|
|
89
|
+
refusal: "refusal",
|
|
90
|
+
refused: "refusal",
|
|
91
|
+
error: "error",
|
|
92
|
+
failed: "error",
|
|
93
|
+
cancelled: "error",
|
|
94
|
+
canceled: "error",
|
|
95
|
+
};
|
|
96
|
+
const mapped = map[lower] ?? map[r];
|
|
97
|
+
if (mapped) return r === mapped ? { finishReason: mapped } : { finishReason: mapped, rawFinishReason: r };
|
|
98
|
+
return { finishReason: "error", rawFinishReason: r };
|
|
99
|
+
}
|
|
100
|
+
|
|
46
101
|
/** JSON representation returned by AgentResponse.toJSON(). */
|
|
47
102
|
export interface AgentResponseJSON {
|
|
48
103
|
text: string;
|
|
104
|
+
/** Validated structured-output value when `AgentConfig.output` was set. */
|
|
105
|
+
parsed?: unknown;
|
|
49
106
|
thinking?: string;
|
|
50
107
|
thoughtSignature?: string;
|
|
51
108
|
toolCalls?: ToolCallRecord[];
|
|
@@ -55,7 +112,9 @@ export interface AgentResponseJSON {
|
|
|
55
112
|
responseId?: string;
|
|
56
113
|
model: string;
|
|
57
114
|
provider: ProviderId | string;
|
|
58
|
-
finishReason?:
|
|
115
|
+
finishReason?: FinishReason;
|
|
116
|
+
/** Original vendor finish string when it differs from canonical `finishReason` (Q-04). */
|
|
117
|
+
rawFinishReason?: string;
|
|
59
118
|
durationMs: number;
|
|
60
119
|
raw: ProviderRawData;
|
|
61
120
|
turns: number;
|
|
@@ -64,6 +123,8 @@ export interface AgentResponseJSON {
|
|
|
64
123
|
/** Normalized final result returned by every Agent run. */
|
|
65
124
|
export class AgentResponse {
|
|
66
125
|
readonly text: string;
|
|
126
|
+
/** Validated structured-output value when `AgentConfig.output` was set. */
|
|
127
|
+
readonly parsed?: unknown;
|
|
67
128
|
readonly thinking?: string;
|
|
68
129
|
readonly thoughtSignature?: string;
|
|
69
130
|
readonly toolCalls: ToolCallRecord[];
|
|
@@ -73,7 +134,9 @@ export class AgentResponse {
|
|
|
73
134
|
readonly responseId?: string;
|
|
74
135
|
readonly model: string;
|
|
75
136
|
readonly provider: ProviderId | string;
|
|
76
|
-
readonly finishReason?:
|
|
137
|
+
readonly finishReason?: FinishReason;
|
|
138
|
+
/** Original vendor finish string when it differs from canonical `finishReason` (Q-04). */
|
|
139
|
+
readonly rawFinishReason?: string;
|
|
77
140
|
readonly durationMs: number;
|
|
78
141
|
readonly raw: ProviderRawData;
|
|
79
142
|
readonly turns: number;
|
|
@@ -87,6 +150,7 @@ export class AgentResponse {
|
|
|
87
150
|
*/
|
|
88
151
|
constructor(data: {
|
|
89
152
|
text: string;
|
|
153
|
+
parsed?: unknown;
|
|
90
154
|
thinking?: string;
|
|
91
155
|
thoughtSignature?: string;
|
|
92
156
|
toolCalls?: ToolCallRecord[];
|
|
@@ -96,12 +160,14 @@ export class AgentResponse {
|
|
|
96
160
|
responseId?: string;
|
|
97
161
|
model: string;
|
|
98
162
|
provider: ProviderId | string;
|
|
99
|
-
finishReason?:
|
|
163
|
+
finishReason?: FinishReason;
|
|
164
|
+
rawFinishReason?: string;
|
|
100
165
|
durationMs: number;
|
|
101
166
|
raw: ProviderRawData;
|
|
102
167
|
turns?: number;
|
|
103
168
|
}) {
|
|
104
169
|
this.text = data.text;
|
|
170
|
+
if (data.parsed !== undefined) (this as { parsed?: unknown }).parsed = data.parsed;
|
|
105
171
|
this.thinking = data.thinking;
|
|
106
172
|
this.thoughtSignature = data.thoughtSignature;
|
|
107
173
|
this.toolCalls = data.toolCalls ?? [];
|
|
@@ -112,6 +178,7 @@ export class AgentResponse {
|
|
|
112
178
|
this.model = data.model;
|
|
113
179
|
this.provider = data.provider;
|
|
114
180
|
this.finishReason = data.finishReason;
|
|
181
|
+
this.rawFinishReason = data.rawFinishReason;
|
|
115
182
|
this.durationMs = data.durationMs;
|
|
116
183
|
this.raw = data.raw;
|
|
117
184
|
this.turns = data.turns ?? 1;
|
|
@@ -126,6 +193,7 @@ export class AgentResponse {
|
|
|
126
193
|
toJSON(): AgentResponseJSON {
|
|
127
194
|
return {
|
|
128
195
|
text: this.text,
|
|
196
|
+
...(this.parsed !== undefined ? { parsed: this.parsed } : {}),
|
|
129
197
|
thinking: this.thinking,
|
|
130
198
|
thoughtSignature: this.thoughtSignature,
|
|
131
199
|
toolCalls: this.toolCalls.length > 0 ? this.toolCalls : undefined,
|
|
@@ -136,6 +204,7 @@ export class AgentResponse {
|
|
|
136
204
|
model: this.model,
|
|
137
205
|
provider: this.provider,
|
|
138
206
|
finishReason: this.finishReason,
|
|
207
|
+
rawFinishReason: this.rawFinishReason,
|
|
139
208
|
durationMs: this.durationMs,
|
|
140
209
|
raw: this.raw,
|
|
141
210
|
turns: this.turns,
|
|
@@ -156,8 +225,12 @@ export type StreamEventType =
|
|
|
156
225
|
| "tool_call_delta"
|
|
157
226
|
| "tool_call_complete"
|
|
158
227
|
| "tool_result"
|
|
228
|
+
| "tool_start"
|
|
229
|
+
| "tool_end"
|
|
159
230
|
| "subagent_complete"
|
|
160
231
|
| "subagent_delta"
|
|
232
|
+
| "steer_injected"
|
|
233
|
+
| "queued"
|
|
161
234
|
| "usage"
|
|
162
235
|
| "done"
|
|
163
236
|
| "error";
|
|
@@ -165,18 +238,75 @@ export type StreamEventType =
|
|
|
165
238
|
/** Payload for one streaming text, thinking, tool, usage, or lifecycle event. */
|
|
166
239
|
export interface StreamEvent {
|
|
167
240
|
type: StreamEventType;
|
|
241
|
+
/** Schema version for typed consumers (Q-40). Currently always 1 when present. */
|
|
242
|
+
schemaVersion?: 1;
|
|
168
243
|
delta?: string;
|
|
169
244
|
thinkingDelta?: string;
|
|
170
245
|
toolCall?: ToolCallRecord;
|
|
171
246
|
toolResult?: ToolResultRecord;
|
|
247
|
+
/** Batch tool calls for `tool_start` events (Q-40). */
|
|
248
|
+
toolCalls?: ToolCallRecord[];
|
|
249
|
+
/** Batch tool results for `tool_end` events (Q-40). */
|
|
250
|
+
toolResults?: ToolResultRecord[];
|
|
172
251
|
subagent?: SubAgentExecutionMetadata;
|
|
173
252
|
/** Tracking id of the worker emitting a `subagent_delta` event. */
|
|
174
253
|
subagentTrackingId?: string;
|
|
254
|
+
/** Steered prompt text injected via `steer()` (`steer_injected` events). */
|
|
255
|
+
injectedPrompt?: string;
|
|
256
|
+
/** Queued prompt text accepted as a next-turn follow-up (`queued` events). */
|
|
257
|
+
queuedPrompt?: string;
|
|
258
|
+
/** Pending queue length at the time of a `queued`/`steer_injected` event. */
|
|
259
|
+
queueLength?: number;
|
|
175
260
|
usage?: TokenUsage;
|
|
176
261
|
responseId?: string;
|
|
262
|
+
/** Canonical finish reason when known; vendor strings pass through as-is
|
|
263
|
+
* for provider compat (normalize with `normalizeFinishReason`). */
|
|
177
264
|
finishReason?: string;
|
|
265
|
+
/** Original vendor finish string when it differs from canonical (Q-04). */
|
|
266
|
+
rawFinishReason?: string;
|
|
178
267
|
error?: Error | unknown;
|
|
179
268
|
partialText?: string;
|
|
180
269
|
partialThinking?: string;
|
|
181
270
|
raw?: ProviderRawData;
|
|
182
271
|
}
|
|
272
|
+
|
|
273
|
+
/** Typed (discriminated) stream event schema, version 1 (Q-40).
|
|
274
|
+
* `StreamEvent` stays the permissive compat shape; use this union when you
|
|
275
|
+
* want exhaustive switching (e.g. `tool_start`/`tool_end` lifecycle). */
|
|
276
|
+
export type StreamEventV2 =
|
|
277
|
+
| { schemaVersion: 1; type: "start"; responseId?: string }
|
|
278
|
+
| { schemaVersion: 1; type: "text_start"; responseId?: string }
|
|
279
|
+
| { schemaVersion: 1; type: "text_delta"; delta: string; partialText?: string }
|
|
280
|
+
| { schemaVersion: 1; type: "text_end"; partialText?: string }
|
|
281
|
+
| { schemaVersion: 1; type: "thinking_start"; responseId?: string }
|
|
282
|
+
| { schemaVersion: 1; type: "thinking_delta"; thinkingDelta: string; partialThinking?: string }
|
|
283
|
+
| { schemaVersion: 1; type: "thinking_end"; partialThinking?: string }
|
|
284
|
+
| { schemaVersion: 1; type: "tool_call_start"; toolCall: ToolCallRecord }
|
|
285
|
+
| { schemaVersion: 1; type: "tool_call_delta"; toolCall: ToolCallRecord; delta?: string }
|
|
286
|
+
| { schemaVersion: 1; type: "tool_call_complete"; toolCall: ToolCallRecord }
|
|
287
|
+
| { schemaVersion: 1; type: "tool_result"; toolResult: ToolResultRecord }
|
|
288
|
+
| { schemaVersion: 1; type: "tool_start"; toolCalls: ToolCallRecord[] }
|
|
289
|
+
| { schemaVersion: 1; type: "tool_end"; toolResults: ToolResultRecord[] }
|
|
290
|
+
| { schemaVersion: 1; type: "subagent_complete"; subagent: SubAgentExecutionMetadata }
|
|
291
|
+
| {
|
|
292
|
+
schemaVersion: 1;
|
|
293
|
+
type: "subagent_delta";
|
|
294
|
+
subagentTrackingId: string;
|
|
295
|
+
delta?: string;
|
|
296
|
+
thinkingDelta?: string;
|
|
297
|
+
partialText?: string;
|
|
298
|
+
partialThinking?: string;
|
|
299
|
+
}
|
|
300
|
+
| { schemaVersion: 1; type: "steer_injected"; injectedPrompt: string; queueLength?: number }
|
|
301
|
+
| { schemaVersion: 1; type: "queued"; queuedPrompt: string; queueLength?: number }
|
|
302
|
+
| { schemaVersion: 1; type: "usage"; usage: TokenUsage }
|
|
303
|
+
| {
|
|
304
|
+
schemaVersion: 1;
|
|
305
|
+
type: "done";
|
|
306
|
+
delta?: string;
|
|
307
|
+
usage?: TokenUsage;
|
|
308
|
+
finishReason?: FinishReason | string;
|
|
309
|
+
rawFinishReason?: string;
|
|
310
|
+
responseId?: string;
|
|
311
|
+
}
|
|
312
|
+
| { schemaVersion: 1; type: "error"; error: Error | unknown };
|
package/src/types/tool.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import type { z } from "zod";
|
|
2
2
|
|
|
3
|
+
/** Minimal logger surface accepted by tool contexts (Q-54/Q-55, additive). */
|
|
4
|
+
export interface ToolLogger {
|
|
5
|
+
debug?: (...args: unknown[]) => void;
|
|
6
|
+
info?: (...args: unknown[]) => void;
|
|
7
|
+
warn?: (...args: unknown[]) => void;
|
|
8
|
+
error?: (...args: unknown[]) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Live-event emitter handed to tools that stream progress (Q-55, additive). */
|
|
12
|
+
export type ToolEmitFn = (event: { type: string; [k: string]: unknown }) => void;
|
|
13
|
+
|
|
3
14
|
export interface ToolExecutionContext {
|
|
4
15
|
/** Unique ID for the model-generated invocation. */
|
|
5
16
|
toolCallId: string;
|
|
@@ -11,6 +22,13 @@ export interface ToolExecutionContext {
|
|
|
11
22
|
sessionId?: string;
|
|
12
23
|
/** Forwards live sub-agent deltas (used by `spawn_subagents` for realtime tracking). */
|
|
13
24
|
onSubagentEvent?: (event: { trackingId: string; delta?: string; thinkingDelta?: string; partialText?: string; partialThinking?: string }) => void;
|
|
25
|
+
// Q-55 Tool API v2 context additions (all optional, additive).
|
|
26
|
+
/** Stable id for this run (`newRunId`). Mirrors SubAgentRunIds.runId. */
|
|
27
|
+
runId?: string;
|
|
28
|
+
/** Live-event emitter for tools that stream progress. */
|
|
29
|
+
emit?: ToolEmitFn;
|
|
30
|
+
/** Structured logger for tool implementations (defaults to no-op). */
|
|
31
|
+
logger?: ToolLogger;
|
|
14
32
|
}
|
|
15
33
|
|
|
16
34
|
export type ToolExecuteFn<TInput = any, TOutput = any> = (
|
|
@@ -37,6 +55,57 @@ export interface ToolDefinition<TInput = any, TOutput = any> {
|
|
|
37
55
|
maxConcurrency?: number | string;
|
|
38
56
|
/** Function executed after validation; its result is made JSON-safe for model context. */
|
|
39
57
|
execute: ToolExecuteFn<TInput, TOutput>;
|
|
58
|
+
// Q-55 Tool API v2 additions (all optional, additive; loop reads via `(toolDef as any).*`).
|
|
59
|
+
/**
|
|
60
|
+
* Canonical alias for `maxTries`. When both are set, `maxTries` wins.
|
|
61
|
+
* Normalized by `normalizeMaxAttempts` (see tools/tool.ts).
|
|
62
|
+
*/
|
|
63
|
+
maxAttempts?: number | string;
|
|
64
|
+
/**
|
|
65
|
+
* Whether this tool is safe to retry on transient failures. Retry defaults
|
|
66
|
+
* to a single attempt unless this is `true` (bounded retries) or `maxTries`/
|
|
67
|
+
* `maxAttempts` explicitly overrides the attempt count.
|
|
68
|
+
*/
|
|
69
|
+
idempotent?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Explicit retry policy. Preferred over bare `maxTries` for new code;
|
|
72
|
+
* `maxTries`/`maxAttempts` map to `{ maxRetries: n - 1 }`.
|
|
73
|
+
*/
|
|
74
|
+
retry?: { maxRetries?: number; baseDelayMs?: number; maxDelayMs?: number };
|
|
75
|
+
/**
|
|
76
|
+
* Whether an identical call may repeat across consecutive turns. Defaults to
|
|
77
|
+
* `true` for idempotent tools, `false` otherwise. Honored by repeat guards.
|
|
78
|
+
*/
|
|
79
|
+
repeatable?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Maximum characters of a string tool result kept for model context.
|
|
82
|
+
* Longer strings are truncated with a `[Truncated: ...]` marker.
|
|
83
|
+
* `0`/omitted uses the executor default (20000).
|
|
84
|
+
*/
|
|
85
|
+
maxResultChars?: number | string;
|
|
86
|
+
/** Optional hook to redact/replace the error message sent back to the model. */
|
|
87
|
+
redactError?: (err: unknown) => string;
|
|
88
|
+
/**
|
|
89
|
+
* Canonical alias for `redactError`. When both are set, `redactError` wins.
|
|
90
|
+
* Kept short for Q-55 Tool API v2 parity.
|
|
91
|
+
*/
|
|
92
|
+
redact?: (err: unknown) => string;
|
|
93
|
+
/** Free-form tags for grouping/filtering tools (e.g. `["web", "readonly"]`). */
|
|
94
|
+
tags?: string[];
|
|
95
|
+
/**
|
|
96
|
+
* When true, the host must approve this call before execution (human-in-the-
|
|
97
|
+
* loop). The loop reads via `(toolDef as any).needsApproval`; enforcement
|
|
98
|
+
* is wired by the host, default open (undefined = no gate).
|
|
99
|
+
*/
|
|
100
|
+
needsApproval?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Optional output schema (Zod or JSON Schema) used to validate/coerce the
|
|
103
|
+
* tool's return value before it reaches model context. Stored as `unknown`
|
|
104
|
+
* so `zod` stays an optional peer for type-only consumers.
|
|
105
|
+
*/
|
|
106
|
+
output?: unknown;
|
|
107
|
+
/** Opaque brand marker narrowing bare object literals to `tool()` output. */
|
|
108
|
+
readonly __brand?: "ToolDefinition";
|
|
40
109
|
}
|
|
41
110
|
|
|
42
111
|
export interface StandardToolDeclaration {
|
package/src/update-models.ts
CHANGED
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
* bun src/update-models.ts --ttl=24h
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { refreshModelCatalog
|
|
11
|
+
import { refreshModelCatalog } from "./index.ts";
|
|
12
|
+
import { ConfigError } from "./types/errors.ts";
|
|
12
13
|
|
|
13
14
|
const args = process.argv.slice(2);
|
|
14
|
-
|
|
15
|
+
// Q-34: force is true only with explicit --force (previously also true when no --ttl).
|
|
16
|
+
const force = args.includes("--force");
|
|
15
17
|
|
|
16
18
|
let customTtlMs: number | undefined;
|
|
17
19
|
for (const arg of args) {
|
|
@@ -24,6 +26,10 @@ for (const arg of args) {
|
|
|
24
26
|
} else {
|
|
25
27
|
customTtlMs = parseFloat(val);
|
|
26
28
|
}
|
|
29
|
+
// Q-34: reject non-numeric TTLs (e.g. --ttl=abc) instead of silently caching NaN.
|
|
30
|
+
if (!Number.isFinite(customTtlMs)) {
|
|
31
|
+
throw new ConfigError(`Invalid --ttl value "${val}": expected a number with optional h/m suffix (e.g. --ttl=24h).`);
|
|
32
|
+
}
|
|
27
33
|
}
|
|
28
34
|
}
|
|
29
35
|
|
package/src/utils/cache.ts
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { CacheRetention } from "../types/core.ts";
|
|
11
|
-
import type { ModelSpec } from "../types/model.ts";
|
|
12
11
|
import { getApiKey } from "./env.ts";
|
|
13
12
|
|
|
14
13
|
const CACHE_KEY_MAX = 64;
|
|
@@ -144,3 +143,71 @@ export async function createExplicitCache(
|
|
|
144
143
|
|
|
145
144
|
return (await response.json()) as CachedContentMetadata;
|
|
146
145
|
}
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Central redaction helpers (Q-11, temporary home to avoid owner conflicts).
|
|
149
|
+
// Providers must call these instead of maintaining local deny-lists.
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
/** Header-name deny-list (case-insensitive substring match). */
|
|
153
|
+
const REDACT_HEADER_SUBSTRINGS = ["key", "token", "secret", "cookie", "authorization"];
|
|
154
|
+
|
|
155
|
+
/** Secret-looking value prefixes. */
|
|
156
|
+
const REDACT_VALUE_PREFIXES = ["sk-", "AIza", "gsk-", "xox-"];
|
|
157
|
+
|
|
158
|
+
function headerNameIsSensitive(name: string): boolean {
|
|
159
|
+
const lower = String(name ?? "").toLowerCase();
|
|
160
|
+
return REDACT_HEADER_SUBSTRINGS.some((s) => lower.includes(s));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function valueLooksSensitive(value: unknown): boolean {
|
|
164
|
+
if (typeof value !== "string") return false;
|
|
165
|
+
const v = value.trim();
|
|
166
|
+
if (REDACT_VALUE_PREFIXES.some((p) => v.includes(p))) return true;
|
|
167
|
+
// Bearer-style long tokens: "Bearer <20+ chars>"
|
|
168
|
+
if (/^bearer\s+\S{20,}/i.test(v)) return true;
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Redacts a single value: sensitive strings become "[REDACTED]" (Q-11).
|
|
174
|
+
*/
|
|
175
|
+
export function redactValue(value: unknown): unknown {
|
|
176
|
+
if (typeof value !== "string") return value;
|
|
177
|
+
if (valueLooksSensitive(value)) return "[REDACTED]";
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Redacts sensitive header values (Q-11). Returns a shallow copy; input untouched.
|
|
183
|
+
* Deny-list (name contains): *key*, *token*, *secret*, cookie, authorization.
|
|
184
|
+
* Value patterns (redacted regardless of name): sk-, AIza, gsk-, xox-.
|
|
185
|
+
*/
|
|
186
|
+
export function redactHeadersCentral(
|
|
187
|
+
headers: Record<string, unknown> | undefined | null
|
|
188
|
+
): Record<string, unknown> {
|
|
189
|
+
if (!headers || typeof headers !== "object") return {};
|
|
190
|
+
const out: Record<string, unknown> = {};
|
|
191
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
192
|
+
if (headerNameIsSensitive(k) || valueLooksSensitive(v)) out[k] = "[REDACTED]";
|
|
193
|
+
else out[k] = v;
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Redacts secret-looking substrings inside free text (Q-11).
|
|
200
|
+
* Replaces `sk-...`, `AIza...`, `gsk-...`, `xox-...` tokens with "[REDACTED]".
|
|
201
|
+
*/
|
|
202
|
+
export function redactText(text: string): string {
|
|
203
|
+
if (typeof text !== "string" || !text) return text;
|
|
204
|
+
// Note: longer prefixes (gsk-/gsk_) first so the generic sk- pattern
|
|
205
|
+
// does not partially match inside them (e.g. "gsk-abc" -> one token).
|
|
206
|
+
return text
|
|
207
|
+
.replace(/gsk_[A-Za-z0-9]{8,}/g, "[REDACTED]")
|
|
208
|
+
.replace(/gsk-[A-Za-z0-9-_]{8,}/g, "[REDACTED]")
|
|
209
|
+
.replace(/xox-[A-Za-z0-9-]{8,}/g, "[REDACTED]")
|
|
210
|
+
.replace(/AIza[A-Za-z0-9-_]{10,}/g, "[REDACTED]")
|
|
211
|
+
.replace(/sk-[A-Za-z0-9-_]{8,}/g, "[REDACTED]")
|
|
212
|
+
.replace(/Bearer\s+[A-Za-z0-9\-._~+/=]{20,}/gi, "Bearer [REDACTED]");
|
|
213
|
+
}
|
package/src/utils/env.ts
CHANGED
|
@@ -2,16 +2,17 @@
|
|
|
2
2
|
* Safe environment lookup for API keys and configurations
|
|
3
3
|
*/
|
|
4
4
|
/**
|
|
5
|
-
* Reads an environment
|
|
5
|
+
* Reads an environment value. Process env only. No globalThis fallback:
|
|
6
|
+
* DOM-clobberable in browsers (Q-05).
|
|
6
7
|
*
|
|
7
8
|
* @example `const apiKey = getEnv("OPENAI_API_KEY");`
|
|
8
9
|
*/
|
|
9
10
|
export function getEnv(key: string, fallback?: string): string | undefined {
|
|
10
11
|
if (typeof process !== "undefined" && process.env && process.env[key]) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
// Trim quotes/whitespace from .env files (`sk-..."\n` -> 401 otherwise).
|
|
13
|
+
const raw = String(process.env[key]);
|
|
14
|
+
const trimmed = raw.trim().replace(/^["']|["']$/g, "").trim();
|
|
15
|
+
if (trimmed) return trimmed;
|
|
15
16
|
}
|
|
16
17
|
return fallback;
|
|
17
18
|
}
|
|
@@ -24,20 +25,31 @@ export type ProviderEnv = Record<string, string>;
|
|
|
24
25
|
* @example `const key = getApiKey("google");`
|
|
25
26
|
*/
|
|
26
27
|
export function getApiKey(provider: string, explicitKey?: string, env?: ProviderEnv): string | undefined {
|
|
27
|
-
|
|
28
|
+
const clean = (v: unknown): string | undefined => {
|
|
29
|
+
if (typeof v !== "string") return undefined;
|
|
30
|
+
const t = v.trim().replace(/^["']|["']$/g, "").trim();
|
|
31
|
+
return t || undefined;
|
|
32
|
+
};
|
|
33
|
+
const cleanedExplicit = clean(explicitKey);
|
|
34
|
+
if (cleanedExplicit) return cleanedExplicit;
|
|
28
35
|
// ProviderEnv override takes precedence (agent-accel inspiration, SDK-light)
|
|
29
36
|
if (env) {
|
|
30
37
|
const upper = `${provider.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_KEY`;
|
|
31
|
-
|
|
38
|
+
const fromEnv = clean(env[upper]);
|
|
39
|
+
if (fromEnv) return fromEnv;
|
|
32
40
|
// google aliases
|
|
33
41
|
if (provider.toLowerCase().startsWith("google") || provider.toLowerCase() === "gemini") {
|
|
34
|
-
|
|
35
|
-
if (
|
|
42
|
+
const g1 = clean(env["GEMINI_API_KEY"]);
|
|
43
|
+
if (g1) return g1;
|
|
44
|
+
const g2 = clean(env["GOOGLE_API_KEY"]);
|
|
45
|
+
if (g2) return g2;
|
|
36
46
|
}
|
|
37
47
|
// openai aliases
|
|
38
48
|
if (provider.toLowerCase() === "openai") {
|
|
39
|
-
|
|
40
|
-
if (
|
|
49
|
+
const o1 = clean(env["OPENAI_BASE_API_KEY"]);
|
|
50
|
+
if (o1) return o1;
|
|
51
|
+
const o2 = clean(env["OPENAI_API_KEY"]);
|
|
52
|
+
if (o2) return o2;
|
|
41
53
|
}
|
|
42
54
|
}
|
|
43
55
|
|
|
@@ -53,8 +65,6 @@ export function getApiKey(provider: string, explicitKey?: string, env?: Provider
|
|
|
53
65
|
return getEnv("OPENROUTER_API_KEY");
|
|
54
66
|
case "openai":
|
|
55
67
|
return getEnv("OPENAI_BASE_API_KEY") || getEnv("OPENAI_API_KEY");
|
|
56
|
-
case "anthropic":
|
|
57
|
-
return getEnv("ANTHROPIC_API_KEY");
|
|
58
68
|
default:
|
|
59
69
|
return getEnv(`${provider.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_KEY`);
|
|
60
70
|
}
|
package/src/utils/errors.ts
CHANGED
|
@@ -54,7 +54,18 @@ export function toConciseProviderError(error: unknown, providerId: string, model
|
|
|
54
54
|
const err = error as any;
|
|
55
55
|
const status = readStatus(err);
|
|
56
56
|
const url = readUrl(err);
|
|
57
|
-
|
|
57
|
+
let detail = readProviderMessage(err);
|
|
58
|
+
// Append OpenRouter-style raw metadata once (pi api/openai-completions.ts:719
|
|
59
|
+
// rule): normalize already stringifies error.error, so only append raw when
|
|
60
|
+
// absent to avoid double-printing.
|
|
61
|
+
const rawMeta =
|
|
62
|
+
(typeof err?.raw === "string" && err.raw.trim() ? err.raw.trim() : undefined) ??
|
|
63
|
+
(typeof err?.error?.metadata?.raw === "string" && err.error.metadata.raw.trim()
|
|
64
|
+
? err.error.metadata.raw.trim()
|
|
65
|
+
: undefined);
|
|
66
|
+
if (rawMeta && !detail.includes(rawMeta.slice(0, 80))) {
|
|
67
|
+
detail = `${detail}\n${rawMeta.slice(0, 500)}`.slice(0, 800);
|
|
68
|
+
}
|
|
58
69
|
const concise = new Error(
|
|
59
70
|
`[${displayModel(providerId, modelId)}] request failed${status ? ` (${status})` : ""}: ${detail}`
|
|
60
71
|
);
|
|
@@ -75,6 +86,9 @@ export function toConciseProviderError(error: unknown, providerId: string, model
|
|
|
75
86
|
...(typeof err?.errorType === "string"
|
|
76
87
|
? { errorType: { value: err.errorType, enumerable: false } }
|
|
77
88
|
: {}),
|
|
89
|
+
...(typeof (err as { raw?: unknown })?.raw === "string"
|
|
90
|
+
? { raw: { value: (err as { raw: string }).raw.slice(0, 500), enumerable: false } }
|
|
91
|
+
: {}),
|
|
78
92
|
});
|
|
79
93
|
return concise;
|
|
80
94
|
}
|