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.
@@ -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?: string;
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?: string;
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?: string;
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 {
@@ -8,10 +8,12 @@
8
8
  * bun src/update-models.ts --ttl=24h
9
9
  */
10
10
 
11
- import { refreshModelCatalog, getCatalogStatus } from "./index.ts";
11
+ import { refreshModelCatalog } from "./index.ts";
12
+ import { ConfigError } from "./types/errors.ts";
12
13
 
13
14
  const args = process.argv.slice(2);
14
- const force = args.includes("--force") || !args.some((a) => a.startsWith("--ttl"));
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
 
@@ -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/global value without assuming a Node-only runtime.
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
- return process.env[key];
12
- }
13
- if (typeof globalThis !== "undefined" && (globalThis as any)[key]) {
14
- return (globalThis as any)[key];
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
- if (explicitKey) return explicitKey;
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
- if (env[upper]) return env[upper];
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
- if (env["GEMINI_API_KEY"]) return env["GEMINI_API_KEY"];
35
- if (env["GOOGLE_API_KEY"]) return env["GOOGLE_API_KEY"];
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
- if (env["OPENAI_BASE_API_KEY"]) return env["OPENAI_BASE_API_KEY"];
40
- if (env["OPENAI_API_KEY"]) return env["OPENAI_API_KEY"];
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
  }
@@ -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
- const detail = readProviderMessage(err);
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
  }