agent-accelerator 0.5.8 → 0.5.9
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/README.md +23 -4
- package/package.json +1 -1
- package/src/agent/agent.ts +8 -6
- package/src/agent/architecture.md +1 -0
- package/src/agent/loop.ts +9 -12
- package/src/agent/worker.ts +2 -1
- package/src/models/architecture.md +1 -1
- package/src/models/catalog.ts +9 -21
- package/src/providers/google.ts +3 -3
- package/src/providers/openai-compat.ts +3 -9
- package/src/providers/openai.ts +3 -2
- package/src/providers/openrouter.ts +2 -2
- package/src/providers/registry.ts +6 -5
- package/src/providers/shared.ts +3 -7
- package/src/scheduler/architecture.md +3 -3
- package/src/utils/architecture.md +2 -2
- package/src/utils/errors.ts +45 -31
- package/src/utils/headers.ts +1 -1
- package/src/utils/retry.ts +2 -28
package/README.md
CHANGED
|
@@ -126,7 +126,7 @@ An `Agent` holds:
|
|
|
126
126
|
| `cache` | `CacheOption` | `"implicit" \| "5m" \| "30m" \| "1h" \| "12h"`. Prompt-cache lifetime; unsupported values warn and fall back (see [Cache](#cache)). |
|
|
127
127
|
| `serviceTier` | `"flex" \| "priority"` | Cost / priority routing where supported. Omit for standard routing. |
|
|
128
128
|
| `maxTurns` | `number` | Maximum model → tool → model loops per `run`. Infinite by default (`0` or omitted = no limit). Hitting a finite limit with pending tool calls warns and sets `finishReason: "max_turns"`. |
|
|
129
|
-
| `limits` | `AgentRunLimits` | `{ maxTurns?, deadlineMs?, maxToolCallsPerRun?, maxTotalTokens?, requestTimeoutMs?, firstByteTimeoutMs?, streamIdleTimeoutMs? }`. The first four are unlimited by default
|
|
129
|
+
| `limits` | `AgentRunLimits` | `{ maxTurns?, deadlineMs?, maxToolCallsPerRun?, maxTotalTokens?, requestTimeoutMs?, firstByteTimeoutMs?, streamIdleTimeoutMs? }`. The first four are unlimited by default; hitting `maxTurns` ends the run with `finishReason: "max_turns"`, the other three throw a `BudgetExceededError`. The three network timeouts default to 300 s / 120 s / 300 s so a silent connection fails instead of hanging; set one to `0` or `Infinity` to turn it off. A timeout raises a retryable `TimeoutError`. See [Safety and operations](docs/guides/safety-and-ops.md#network-timeouts). |
|
|
130
130
|
| `budget` | `Budget` | `{ maxCostUsd?, maxTotalTokens?, maxDurationMs?, maxTotalSubagents? }`, per run, workers included. Unlimited by default; exceeding it throws `BudgetExceededError` with partial usage. |
|
|
131
131
|
| `sessionId` | `string` | Stable identifier used for cache affinity. Auto-generated when omitted. |
|
|
132
132
|
| `headers` | `Record<string,string>` | Additional headers merged into every request. |
|
|
@@ -1089,16 +1089,35 @@ Credential headers are always redacted (`Authorization`, `x-goog-api-key`, anyth
|
|
|
1089
1089
|
---
|
|
1090
1090
|
## Errors
|
|
1091
1091
|
|
|
1092
|
-
Provider failures throw `
|
|
1092
|
+
Provider failures throw a `ProviderError` — one actionable line,
|
|
1093
1093
|
never a wire dump:
|
|
1094
1094
|
|
|
1095
1095
|
```text
|
|
1096
1096
|
[openrouter/nvidia/nemotron-3.5-lightning:free] request failed (404): No endpoints found that support input video
|
|
1097
1097
|
```
|
|
1098
1098
|
|
|
1099
|
-
|
|
1099
|
+
Branch on the class and `err.retryable` (true for HTTP 408/425/429/5xx,
|
|
1100
|
+
network errors and a cut stream), never on the message:
|
|
1101
|
+
|
|
1102
|
+
```ts
|
|
1103
|
+
import { ProviderError, TimeoutError } from "agent-accelerator";
|
|
1104
|
+
|
|
1105
|
+
try {
|
|
1106
|
+
await agent.run("...");
|
|
1107
|
+
} catch (err) {
|
|
1108
|
+
if (err instanceof ProviderError && err.retryable) retryLater(); // e.g. a 429 or 503
|
|
1109
|
+
else if (err instanceof TimeoutError) retryLater(); // a request / first-byte / idle timeout
|
|
1110
|
+
else throw err;
|
|
1111
|
+
}
|
|
1112
|
+
```
|
|
1113
|
+
|
|
1114
|
+
`err.code` is the SDK kind (`"provider"`, `"incomplete_stream"` for a cut
|
|
1115
|
+
stream; `"timeout"`, `"config"`, ... on the other classes); the provider's own
|
|
1116
|
+
error code (e.g. `"not_found"`, `"rate_limit_exceeded"`) is `err.providerCode`.
|
|
1117
|
+
Raw details (`statusCode`, `url`, truncated body, `providerCode`) stay attached as
|
|
1100
1118
|
non-enumerable properties: available programmatically, invisible in runtime
|
|
1101
|
-
dumps.
|
|
1119
|
+
dumps. Input a model cannot take (a modality the catalog says it lacks, video on
|
|
1120
|
+
the OpenAI Responses wire) fails before the network with a `ValidationError`. URL query strings are stripped (keys sometimes live there); headers
|
|
1102
1121
|
and cookies are never attached. Helpers:
|
|
1103
1122
|
|
|
1104
1123
|
* `toConciseProviderError(err, providerId, modelId)` — collapse any provider error.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-accelerator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
4
4
|
"description": "High-performance SDK for AI agents with a unified API across AI providers. Optimized for high cache hit rates, observability, and seamless agent orchestration. Built to power production-grade agentic systems and harnesses for teams at any scale.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Sashvat Bharat",
|
package/src/agent/agent.ts
CHANGED
|
@@ -63,7 +63,7 @@ export function resolveEffectiveThinking(
|
|
|
63
63
|
return { enabled: true, level: overrideLevel as ThinkingConfig["level"] };
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
import { ConfigError, SessionLoadError, isAbortError } from "../types/errors.ts";
|
|
66
|
+
import { ConfigError, SessionLoadError, ValidationError, isAbortError } from "../types/errors.ts";
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* Disposers for the manual user-signal forward listeners (fallback when
|
|
@@ -697,13 +697,15 @@ export class Agent {
|
|
|
697
697
|
return out || undefined;
|
|
698
698
|
}
|
|
699
699
|
|
|
700
|
-
/**
|
|
700
|
+
/** Starts a fresh conversation: clears messages, signatures, run marks and the run's saved tool results, keeps config and spending totals. */
|
|
701
701
|
reset(): void {
|
|
702
702
|
const sid = this.sessionId;
|
|
703
703
|
this.context.messages = [];
|
|
704
704
|
this.context.thoughtSignatures = [];
|
|
705
705
|
this.context.systemPrompt = this.getFullInstructions();
|
|
706
706
|
this.runMarks.length = 0;
|
|
707
|
+
this.toolRecords.length = 0; // a cleared conversation has no run to resume, so its finished tool results go too
|
|
708
|
+
this.activeMark = undefined;
|
|
707
709
|
// Reset also drops session-scoped provider chaining/routing.
|
|
708
710
|
try { clearInteractionChains(sid); } catch {}
|
|
709
711
|
try { clearSessionRouting(sid); } catch {}
|
|
@@ -837,7 +839,7 @@ export class Agent {
|
|
|
837
839
|
// Throws synchronously on an empty content-part array so run() and stream() fail at the same moment; in: any prompt entry point, out: nothing.
|
|
838
840
|
private assertPromptValid(prompt: string | ContentPart[]): void {
|
|
839
841
|
if (Array.isArray(prompt) && prompt.length === 0) {
|
|
840
|
-
throw new
|
|
842
|
+
throw new ValidationError(
|
|
841
843
|
"[Agent Accelerator] Prompt cannot be empty: pass a non-empty string or at least one content part."
|
|
842
844
|
);
|
|
843
845
|
}
|
|
@@ -1693,7 +1695,7 @@ export class Agent {
|
|
|
1693
1695
|
// Prompt validation throws synchronously in both run() and
|
|
1694
1696
|
// stream() (same timing; documented here).
|
|
1695
1697
|
if (Array.isArray(prompt) && prompt.length === 0) {
|
|
1696
|
-
throw new
|
|
1698
|
+
throw new ValidationError(
|
|
1697
1699
|
"[Agent Accelerator] Prompt cannot be empty: pass a non-empty string or at least one content part."
|
|
1698
1700
|
);
|
|
1699
1701
|
}
|
|
@@ -2254,7 +2256,7 @@ export class Agent {
|
|
|
2254
2256
|
/** What a failed run attempt got done before it failed (the `partial` the loop attaches to its error). */
|
|
2255
2257
|
type FailedAttempt = Partial<AgentResponse> & { usage: TokenUsage };
|
|
2256
2258
|
|
|
2257
|
-
// Adds what failed attempts already did (tool calls, results, turns, token usage) to the response of the attempt that finally worked, so a retry never hides side effects or cost; in: the final response + each failed attempt's `err.partial`, out: the run's AgentResponse.
|
|
2259
|
+
// Adds what failed attempts already did (tool calls, results, workers, turns, token usage) to the response of the attempt that finally worked, so a retry never hides side effects or cost; in: the final response + each failed attempt's `err.partial`, out: the run's AgentResponse.
|
|
2258
2260
|
function withFailedAttempts(res: AgentResponse, failed: FailedAttempt[]): AgentResponse {
|
|
2259
2261
|
const usage: TokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
2260
2262
|
for (const f of failed) accumulateUsage(usage, f.usage);
|
|
@@ -2266,7 +2268,7 @@ function withFailedAttempts(res: AgentResponse, failed: FailedAttempt[]): AgentR
|
|
|
2266
2268
|
thoughtSignature: res.thoughtSignature,
|
|
2267
2269
|
toolCalls: [...failed.flatMap((f) => f.toolCalls ?? []), ...res.toolCalls],
|
|
2268
2270
|
toolResults: [...failed.flatMap((f) => f.toolResults ?? []), ...res.toolResults],
|
|
2269
|
-
subagents: res.subagents,
|
|
2271
|
+
subagents: [...failed.flatMap((f) => f.subagents ?? []), ...res.subagents],
|
|
2270
2272
|
usage,
|
|
2271
2273
|
responseId: res.responseId,
|
|
2272
2274
|
model: res.model,
|
|
@@ -23,3 +23,4 @@ The agent and everything that runs under it.
|
|
|
23
23
|
- Exactly-once worker calls rest on the call key (parent run mark id + tool call id) and `meta.last`.
|
|
24
24
|
- Only results branded by `makeSubagentResult` have their usage metadata trusted.
|
|
25
25
|
- Each Agent records only its own model turns; never sum `AgentResponse.usage` for totals.
|
|
26
|
+
- `reset()` clears the conversation (messages, run marks, the run's saved tool results) but never the spending totals.
|
package/src/agent/loop.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
import { getModelFromCatalog, ensureModelCatalogFresh, validateModelThinking } from "../models/catalog.ts";
|
|
24
24
|
import { enforceFileAccess } from "../utils/safe-io.ts";
|
|
25
25
|
import { isSubagentResult, normalizeSubagentMetadata } from "../tools/subagent-result.ts";
|
|
26
|
-
import { BudgetExceededError } from "../types/errors.ts";
|
|
26
|
+
import { BudgetExceededError, TimeoutError } from "../types/errors.ts";
|
|
27
27
|
import { applyCaptureRaw, type CaptureRawMode } from "../utils/raw-capture.ts";
|
|
28
28
|
import { previewPrompt } from "../utils/session.ts";
|
|
29
29
|
import { noteProviderTurn, normalizeStructuredOutput, parseStructuredOutput } from "../providers.ts";
|
|
@@ -389,7 +389,7 @@ function checkTokenLimit(usage: TokenUsage, maxTotalTokens: number): void {
|
|
|
389
389
|
// Throws when the run has been going longer than limits.deadlineMs; in: both loops each turn, out: nothing or a thrown error.
|
|
390
390
|
function checkDeadline(startTime: number, deadlineMs: number): void {
|
|
391
391
|
if (Number.isFinite(deadlineMs) && deadlineMs !== Infinity && Date.now() - startTime > deadlineMs) {
|
|
392
|
-
throw new
|
|
392
|
+
throw new BudgetExceededError(`[Agent Accelerator] Run deadline exceeded after ${deadlineMs}ms.`);
|
|
393
393
|
}
|
|
394
394
|
}
|
|
395
395
|
|
|
@@ -400,9 +400,7 @@ async function generateWithTimeout<T>(task: () => Promise<T>, timeoutMs: number)
|
|
|
400
400
|
try {
|
|
401
401
|
const timeoutP = new Promise<never>((_, reject) => {
|
|
402
402
|
timer = setTimeout(() => {
|
|
403
|
-
|
|
404
|
-
e.name = "TimeoutError";
|
|
405
|
-
reject(e);
|
|
403
|
+
reject(new TimeoutError(`[Agent Accelerator] Request timed out after ${timeoutMs}ms.`));
|
|
406
404
|
}, timeoutMs);
|
|
407
405
|
try { (timer as unknown as { unref?: () => void }).unref?.(); } catch {}
|
|
408
406
|
});
|
|
@@ -859,7 +857,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
859
857
|
allToolCalls.push(...genResult.toolCalls);
|
|
860
858
|
// Per-run tool-call budget.
|
|
861
859
|
if (allToolCalls.length > limits.maxToolCallsPerRun) {
|
|
862
|
-
throw new
|
|
860
|
+
throw new BudgetExceededError(
|
|
863
861
|
`[Agent Accelerator] maxToolCallsPerRun exceeded (${allToolCalls.length} > ${limits.maxToolCallsPerRun}).`
|
|
864
862
|
);
|
|
865
863
|
}
|
|
@@ -998,6 +996,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
998
996
|
turns,
|
|
999
997
|
toolCalls: allToolCalls,
|
|
1000
998
|
toolResults: allToolResults,
|
|
999
|
+
subagents: allSubagents,
|
|
1001
1000
|
text: finalResult?.text,
|
|
1002
1001
|
};
|
|
1003
1002
|
} catch {
|
|
@@ -1191,11 +1190,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1191
1190
|
Number.isFinite(waitMs) && waitMs !== Infinity && waitMs > 0
|
|
1192
1191
|
? new Promise<never>((_, reject) => {
|
|
1193
1192
|
timer = setTimeout(() => {
|
|
1194
|
-
|
|
1195
|
-
`[Agent Accelerator] Stream ${first ? "first-byte" : "idle"} timed out after ${waitMs}ms.`
|
|
1196
|
-
);
|
|
1197
|
-
e.name = "TimeoutError";
|
|
1198
|
-
reject(e);
|
|
1193
|
+
reject(new TimeoutError(`[Agent Accelerator] Stream ${first ? "first-byte" : "idle"} timed out after ${waitMs}ms.`));
|
|
1199
1194
|
}, waitMs);
|
|
1200
1195
|
try { (timer as unknown as { unref?: () => void }).unref?.(); } catch {}
|
|
1201
1196
|
})
|
|
@@ -1311,7 +1306,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1311
1306
|
emitProgress(config); // the calls are on record BEFORE they run, so a crash can finish them instead of forgetting them
|
|
1312
1307
|
allToolCalls.push(...turnResponse.toolCalls);
|
|
1313
1308
|
if (allToolCalls.length > streamLimits.maxToolCallsPerRun) {
|
|
1314
|
-
throw new
|
|
1309
|
+
throw new BudgetExceededError(
|
|
1315
1310
|
`[Agent Accelerator] maxToolCallsPerRun exceeded (${allToolCalls.length} > ${streamLimits.maxToolCallsPerRun}).`
|
|
1316
1311
|
);
|
|
1317
1312
|
}
|
|
@@ -1444,6 +1439,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1444
1439
|
turns,
|
|
1445
1440
|
toolCalls: allToolCalls,
|
|
1446
1441
|
toolResults: allToolResults,
|
|
1442
|
+
subagents: allSubagents,
|
|
1447
1443
|
text: lastResponse?.text,
|
|
1448
1444
|
}; } catch {}
|
|
1449
1445
|
outerStream.fail(raw);
|
|
@@ -1504,6 +1500,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1504
1500
|
turns,
|
|
1505
1501
|
toolCalls: allToolCalls,
|
|
1506
1502
|
toolResults: allToolResults,
|
|
1503
|
+
subagents: allSubagents,
|
|
1507
1504
|
text: lastResponse?.text,
|
|
1508
1505
|
};
|
|
1509
1506
|
// Aborts always surface as AbortError with partial attached.
|
package/src/agent/worker.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { Agent } from "./agent.ts";
|
|
|
5
5
|
import { normalizeToolName } from "../tools/executor.ts";
|
|
6
6
|
import { getModel, getSubModel } from "../utils/env.ts";
|
|
7
7
|
import { resolveModel } from "../providers/registry.ts";
|
|
8
|
+
import { ConfigError } from "../types/errors.ts";
|
|
8
9
|
|
|
9
10
|
/** Minimal live-trace shape shared by fixed + dynamic workers (structural subset of `SubAgentTrace`). */
|
|
10
11
|
export interface LiveWorkerTrace {
|
|
@@ -134,7 +135,7 @@ export function resolveDynamicModelInput(
|
|
|
134
135
|
export function resolveSubAgentModel(config: { name?: string; model?: AgentConfig["model"] }): NonNullable<AgentConfig["model"]> {
|
|
135
136
|
const resolvedModel = config.model ?? getSubModel() ?? getModel();
|
|
136
137
|
if (!resolvedModel) {
|
|
137
|
-
throw new
|
|
138
|
+
throw new ConfigError(
|
|
138
139
|
`[Agent Accelerator] SubAgent '${config.name || "subagent"}' requires a model. ` +
|
|
139
140
|
`Specify 'model' in SubAgent config, or set SUB_AGENT_MODEL or MODEL in environment variables.`
|
|
140
141
|
);
|
|
@@ -5,7 +5,7 @@ The model catalog: context limits, prices, modalities and reasoning options come
|
|
|
5
5
|
| File | What it is |
|
|
6
6
|
| :-- | :-- |
|
|
7
7
|
| `catalog-cache.ts` | Downloads and caches `models.dev/api.json` in `<home>/catalog/` (or `AGENT_CACHE_DIR`). Stale-while-revalidate: an expired catalog is served at once and refreshed in the background (8 s timeout, 10 min backoff after a failure); only a cold start waits. `AGENT_CATALOG_REFRESH=off` stops automatic refreshes. |
|
|
8
|
-
| `catalog.ts` | Lookups (`getModelFromCatalog`), `registerModel`, thinking validation (`validateModelThinking`), and the exported `GOOGLE_/OPENAI_/OPENROUTER_MODELS` arrays (rebuilt in place when the catalog changes). |
|
|
8
|
+
| `catalog.ts` | Lookups (`getModelFromCatalog`), `registerModel`, thinking validation (`validateModelThinking`, which throws `ThinkingLevelError`, a one-line `ConfigError`), and the exported `GOOGLE_/OPENAI_/OPENROUTER_MODELS` arrays (rebuilt in place when the catalog changes). |
|
|
9
9
|
|
|
10
10
|
## Invariants
|
|
11
11
|
- Never hand-edit the cache file; register custom models with `registerModel()`. Verified modality fixes go in `MODALITY_OVERRIDES`.
|
package/src/models/catalog.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "./catalog-cache.ts";
|
|
20
20
|
import type { ModelSpec, ProviderId, ModelLimit, ModelCost, ModelModalities } from "../types/model.ts";
|
|
21
21
|
import type { ThinkingLevel } from "../types/core.ts";
|
|
22
|
+
import { ConfigError } from "../types/errors.ts";
|
|
22
23
|
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
24
25
|
// Provider aliases — maps our internal ids to models.dev keys
|
|
@@ -421,8 +422,8 @@ export function getModelThinkingInfo(provider: string, modelId: string): ModelTh
|
|
|
421
422
|
};
|
|
422
423
|
}
|
|
423
424
|
|
|
424
|
-
/** Thrown when a requested ThinkingLevel is unsupported by the selected model
|
|
425
|
-
export class ThinkingLevelError extends
|
|
425
|
+
/** Thrown when a requested ThinkingLevel is unsupported by the selected model: a `ConfigError` (one line, never retried) with `allowedLevels`. */
|
|
426
|
+
export class ThinkingLevelError extends ConfigError {
|
|
426
427
|
readonly provider: string;
|
|
427
428
|
readonly modelId: string;
|
|
428
429
|
readonly requestedLevel: string;
|
|
@@ -443,31 +444,18 @@ export class ThinkingLevelError extends Error {
|
|
|
443
444
|
reason: string;
|
|
444
445
|
remedy?: string;
|
|
445
446
|
}) {
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const formattedMessage =
|
|
454
|
-
`[Agent Accelerator] ThinkingLevel Mismatch for "${opts.provider}/${opts.modelId}":\n` +
|
|
455
|
-
` Requested Level: "${opts.requestedLevel}"\n` +
|
|
456
|
-
` Issue: ${opts.reason}` +
|
|
457
|
-
allowedLine +
|
|
458
|
-
remedyLines;
|
|
459
|
-
|
|
460
|
-
super(formattedMessage);
|
|
447
|
+
const allowed = opts.allowedLevels.length > 0 ? ` Allowed levels: ${opts.allowedLevels.map((l) => `"${l}"`).join(", ")}.` : "";
|
|
448
|
+
const remedy = opts.remedy ? ` How to fix: ${opts.remedy}` : "";
|
|
449
|
+
super(
|
|
450
|
+
`[Agent Accelerator] Thinking level "${opts.requestedLevel}" does not fit "${opts.provider}/${opts.modelId}": ${opts.reason}${allowed}${remedy}`,
|
|
451
|
+
{ provider: opts.provider, model: opts.modelId }
|
|
452
|
+
);
|
|
461
453
|
this.name = "ThinkingLevelError";
|
|
462
454
|
this.provider = opts.provider;
|
|
463
455
|
this.modelId = opts.modelId;
|
|
464
456
|
this.requestedLevel = opts.requestedLevel;
|
|
465
457
|
this.allowedLevels = opts.allowedLevels;
|
|
466
458
|
this.supportsThinking = opts.supportsThinking;
|
|
467
|
-
|
|
468
|
-
if (Error.captureStackTrace) {
|
|
469
|
-
Error.captureStackTrace(this, ThinkingLevelError);
|
|
470
|
-
}
|
|
471
459
|
}
|
|
472
460
|
}
|
|
473
461
|
|
package/src/providers/google.ts
CHANGED
|
@@ -383,7 +383,7 @@ function cleanModelId(model: string | ModelSpec): string {
|
|
|
383
383
|
function assertSupportedGeneration(clean: string): void {
|
|
384
384
|
const match = clean.match(/^gemini-(\d+)(?:[.\-]|$)/i);
|
|
385
385
|
if (match && Number(match[1]) < 3) {
|
|
386
|
-
throw new
|
|
386
|
+
throw new ConfigError(
|
|
387
387
|
`[Agent Accelerator] [google/${clean}] Unsupported generation: Gemini ${match[1]}.x and older are strictly rejected by this SDK. ` +
|
|
388
388
|
`Use a Gemini 3 or later model for full support and feature coverage.`
|
|
389
389
|
);
|
|
@@ -1022,7 +1022,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1022
1022
|
assertSupportedGeneration(clean);
|
|
1023
1023
|
const apiKey = resolveApiKey(options);
|
|
1024
1024
|
if (!apiKey) {
|
|
1025
|
-
throw new
|
|
1025
|
+
throw new ConfigError(
|
|
1026
1026
|
"[Agent Accelerator] Missing Google API key. Set GEMINI_API_KEY (or GOOGLE_API_KEY) or pass apiKey."
|
|
1027
1027
|
);
|
|
1028
1028
|
}
|
|
@@ -1133,7 +1133,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1133
1133
|
assertSupportedGeneration(clean);
|
|
1134
1134
|
const apiKey = resolveApiKey(options);
|
|
1135
1135
|
if (!apiKey) {
|
|
1136
|
-
throw new
|
|
1136
|
+
throw new ConfigError(
|
|
1137
1137
|
"[Agent Accelerator] Missing Google API key. Set GEMINI_API_KEY (or GOOGLE_API_KEY) or pass apiKey."
|
|
1138
1138
|
);
|
|
1139
1139
|
}
|
|
@@ -59,7 +59,7 @@ import { normalizeMediaInput } from "../utils/media.ts";
|
|
|
59
59
|
import { safeStringify } from "../utils/serialization.ts";
|
|
60
60
|
import { toConciseProviderError, assertModalitiesSupported } from "../utils/errors.ts";
|
|
61
61
|
import { withRetries } from "../utils/retry.ts";
|
|
62
|
-
import { ConfigError } from "../types/errors.ts";
|
|
62
|
+
import { ConfigError, ValidationError } from "../types/errors.ts";
|
|
63
63
|
import {
|
|
64
64
|
parseArguments,
|
|
65
65
|
normalizeThinkingParts,
|
|
@@ -350,13 +350,7 @@ function assertNoVideoOrFileParts(
|
|
|
350
350
|
kind === "video"
|
|
351
351
|
? `[${prefix}/${modelId}] unsupported video input (generic OpenAI-compatible endpoints carry text/image/audio only). Use a video-capable model (e.g. google/gemini-*) or drop the video part.`
|
|
352
352
|
: `[${prefix}/${modelId}] unsupported file input (generic OpenAI-compatible endpoints carry text/image/audio only). Use a file-capable model (e.g. google/gemini-*) or convert the document to text before sending it.`;
|
|
353
|
-
|
|
354
|
-
err.name = "AgentAccelProviderError";
|
|
355
|
-
Object.defineProperties(err, {
|
|
356
|
-
provider: { value: prefix, enumerable: false },
|
|
357
|
-
model: { value: modelId, enumerable: false },
|
|
358
|
-
});
|
|
359
|
-
throw err;
|
|
353
|
+
throw new ValidationError(message, { provider: prefix, model: modelId });
|
|
360
354
|
}
|
|
361
355
|
|
|
362
356
|
// Text, image (link or data URL) and audio parts as Chat content blocks; in: a user message's parts, out: fullHistoryMessages.
|
|
@@ -701,7 +695,7 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
701
695
|
private requireApiKey(baseUrl: string, options?: ProviderRequestOptions): string | undefined {
|
|
702
696
|
const key = this.apiKey(options);
|
|
703
697
|
if (!key && !isLocalEndpoint(baseUrl)) {
|
|
704
|
-
throw new
|
|
698
|
+
throw new ConfigError(
|
|
705
699
|
`[Agent Accelerator] Missing API key for "${this.prefix}". Set ${envPrefixOf(this.prefix)}_API_KEY or pass apiKey. (Local endpoints may omit the key.)`
|
|
706
700
|
);
|
|
707
701
|
}
|
package/src/providers/openai.ts
CHANGED
|
@@ -51,6 +51,7 @@ import { clampCacheKey } from "../utils/cache.ts";
|
|
|
51
51
|
import { normalizeMediaInput } from "../utils/media.ts";
|
|
52
52
|
import { safeStringify } from "../utils/serialization.ts";
|
|
53
53
|
import { toConciseProviderError, assertModalitiesSupported, assertNoVideoPartsOnResponses } from "../utils/errors.ts";
|
|
54
|
+
import { ConfigError } from "../types/errors.ts";
|
|
54
55
|
import { withRetries } from "../utils/retry.ts";
|
|
55
56
|
import {
|
|
56
57
|
parseArguments,
|
|
@@ -716,7 +717,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
716
717
|
const clean = cleanModelId(model);
|
|
717
718
|
const apiKey = resolveApiKey(options);
|
|
718
719
|
if (!apiKey) {
|
|
719
|
-
throw new
|
|
720
|
+
throw new ConfigError(
|
|
720
721
|
"[Agent Accelerator] Missing OpenAI API key. Set OPENAI_API_KEY (or OPENAI_BASE_API_KEY) or pass apiKey."
|
|
721
722
|
);
|
|
722
723
|
}
|
|
@@ -804,7 +805,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
804
805
|
try {
|
|
805
806
|
const apiKey = resolveApiKey(options);
|
|
806
807
|
if (!apiKey) {
|
|
807
|
-
throw new
|
|
808
|
+
throw new ConfigError(
|
|
808
809
|
"[Agent Accelerator] Missing OpenAI API key. Set OPENAI_API_KEY (or OPENAI_BASE_API_KEY) or pass apiKey."
|
|
809
810
|
);
|
|
810
811
|
}
|
|
@@ -46,7 +46,7 @@ import { buildSessionHeaders, setHeaderCaseInsensitive } from "../utils/headers.
|
|
|
46
46
|
import { normalizeMediaInput } from "../utils/media.ts";
|
|
47
47
|
import { safeStringify } from "../utils/serialization.ts";
|
|
48
48
|
import { toConciseProviderError, assertModalitiesSupported } from "../utils/errors.ts";
|
|
49
|
-
import { isAbortError } from "../types/errors.ts";
|
|
49
|
+
import { ConfigError, isAbortError } from "../types/errors.ts";
|
|
50
50
|
import {
|
|
51
51
|
newToolCallId,
|
|
52
52
|
redactedHeaders,
|
|
@@ -648,7 +648,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
648
648
|
try {
|
|
649
649
|
const apiKey = resolveApiKey(options);
|
|
650
650
|
if (!apiKey) {
|
|
651
|
-
throw new
|
|
651
|
+
throw new ConfigError(
|
|
652
652
|
"[Agent Accelerator] Missing OpenRouter API key. Set OPENROUTER_API_KEY or pass apiKey."
|
|
653
653
|
);
|
|
654
654
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ConfigError } from "../types/errors.ts";
|
|
1
2
|
import type { Provider, ProviderId, ModelSpec } from "../types/model.ts";
|
|
2
3
|
import type { ThinkingLevel } from "../types/core.ts";
|
|
3
4
|
import { OpenAICompatibleChatProvider } from "./openai-compat.ts";
|
|
@@ -54,7 +55,7 @@ function removedProviderMessage(id: string): string {
|
|
|
54
55
|
/** Throws for removed provider prefixes. */
|
|
55
56
|
function assertProviderNotRemoved(id: string): void {
|
|
56
57
|
if (REMOVED_PROVIDERS.has(id.trim().toLowerCase())) {
|
|
57
|
-
throw new
|
|
58
|
+
throw new ConfigError(removedProviderMessage(id.trim()));
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
|
|
@@ -83,7 +84,7 @@ export function ensureCustomProvider(
|
|
|
83
84
|
const id = normalizeProviderPrefix(prefix) as ProviderId;
|
|
84
85
|
if (FIRST_CLASS.has(id.toLowerCase())) {
|
|
85
86
|
if (opts?.baseUrl !== undefined || opts?.apiKey !== undefined) {
|
|
86
|
-
throw new
|
|
87
|
+
throw new ConfigError(
|
|
87
88
|
`[Agent Accelerator] Provider "${id}" is first-class and cannot be replaced with custom baseUrl/apiKey. Pass baseUrl/apiKey per-Agent instead.`
|
|
88
89
|
);
|
|
89
90
|
}
|
|
@@ -93,7 +94,7 @@ export function ensureCustomProvider(
|
|
|
93
94
|
if (existing) {
|
|
94
95
|
const prevBase = (existing as { configuredBaseUrl?: string }).configuredBaseUrl;
|
|
95
96
|
if (opts?.baseUrl !== undefined && prevBase !== undefined && prevBase !== opts.baseUrl) {
|
|
96
|
-
throw new
|
|
97
|
+
throw new ConfigError(
|
|
97
98
|
`[Agent Accelerator] Conflicting re-registration for provider "${id}". Registry is immutable after creation.`
|
|
98
99
|
);
|
|
99
100
|
}
|
|
@@ -120,7 +121,7 @@ export function getProvider(id: ProviderId | string): Provider {
|
|
|
120
121
|
const provider = providerRegistry.get(key);
|
|
121
122
|
if (provider) return provider;
|
|
122
123
|
if (!key) {
|
|
123
|
-
throw new
|
|
124
|
+
throw new ConfigError(
|
|
124
125
|
`Provider '' is not supported or not registered. Available first-support providers: google, openrouter, openai — or any custom prefix via {PREFIX}_API_KEY + {PREFIX}_BASE_URL.`
|
|
125
126
|
);
|
|
126
127
|
}
|
|
@@ -146,7 +147,7 @@ export interface ResolvedModel {
|
|
|
146
147
|
*/
|
|
147
148
|
export function resolveModel(model: string | ModelSpec): ResolvedModel {
|
|
148
149
|
if (!model || (typeof model === "string" && !model.trim())) {
|
|
149
|
-
throw new
|
|
150
|
+
throw new ConfigError(
|
|
150
151
|
"[Agent Accelerator] No model specified. Please specify 'model' in Agent configuration or set the MODEL environment variable."
|
|
151
152
|
);
|
|
152
153
|
}
|
package/src/providers/shared.ts
CHANGED
|
@@ -78,7 +78,7 @@ export function readErrorPayload(
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
/** Retryable incomplete-stream error for truncated SSE
|
|
81
|
+
/** Retryable incomplete-stream error for truncated SSE: a `ProviderError` whose `code` is `incomplete_stream`. */
|
|
82
82
|
export function incompleteStreamError(provider: string, modelId: string): Error {
|
|
83
83
|
const err = toConciseProviderError(
|
|
84
84
|
Object.assign(new Error("Stream ended without terminal event"), {
|
|
@@ -110,12 +110,8 @@ export function isNotFoundChainError(error: unknown): boolean {
|
|
|
110
110
|
NaN
|
|
111
111
|
);
|
|
112
112
|
if (status === 404) return true;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
(err["errorType"] as string | undefined) ??
|
|
116
|
-
""
|
|
117
|
-
).toLowerCase();
|
|
118
|
-
return code === "not_found";
|
|
113
|
+
// `providerCode` on a wrapped ProviderError (its `code` is the SDK kind), `code` on a raw error.
|
|
114
|
+
return [err["providerCode"], err["code"], err["errorType"]].some((c) => String(c ?? "").toLowerCase() === "not_found");
|
|
119
115
|
}
|
|
120
116
|
|
|
121
117
|
/** Per-request timeout ms from provider options. */
|
|
@@ -4,18 +4,18 @@ Scheduled actions: a worker that runs later (once or repeating) and saves its an
|
|
|
4
4
|
|
|
5
5
|
| File | What it is |
|
|
6
6
|
| :-- | :-- |
|
|
7
|
-
| `scheduler.ts` | One `Scheduler` per home folder: a 1 s unref'd clock that runs only while something is pending, startup sweep, cook/deliver state machine, and the public functions (`createScheduledAction`, `list/update/run/skip/deleteScheduledAction`). |
|
|
7
|
+
| `scheduler.ts` | One `Scheduler` per home folder: a 1 s unref'd clock that runs only while something is pending, startup sweep, dead-owner recovery, heartbeats, cook/deliver state machine, and the public functions (`createScheduledAction`, `list/update/run/skip/deleteScheduledAction`). |
|
|
8
8
|
| `store.ts` | Disk layer under `<home>/scheduled/`: `<id>/task.json`, `<id>/delivery.json`, run files, one short mutex per action; validation of records. |
|
|
9
9
|
| `time.ts` | Pure `DD:HH:MM` and local-clock maths. No timezone, DST or `Intl`; `now` is always passed in. |
|
|
10
10
|
| `tool.ts` + `scheduled-actions.md` | The model-facing `scheduled_actions` tool and `get_datetime`; the `.md` is the tool description. |
|
|
11
11
|
|
|
12
12
|
## Invariants
|
|
13
13
|
- The files are the truth: every write re-reads first and goes through the durable writer. The file format is a stable public contract: see `docs/scheduled-action-files.md` and add a changelog row for any change.
|
|
14
|
-
- Write order when a run ends: run file, then `<id>/delivery.json`, then `<id>/task.json`.
|
|
14
|
+
- Write order when a run ends: run file, then `<id>/delivery.json`, then `<id>/task.json`. A crash in between is repaired by the dead-owner recovery (`recoverCooking`, at start and every 15 s): the delivery is rebuilt from the run file, or the run is marked `missed` when no run file exists.
|
|
15
15
|
- `startCook` advances the next times BEFORE the run starts; a run still cooking when the next is due is skipped, never stacked.
|
|
16
16
|
- Runs are fresh stateless workers built by `buildDynamicWorkerConfig`; their spending guardrail is `workerBudget` only.
|
|
17
17
|
- The Agent registry holds `WeakRef`s by name: keep every Agent that owns actions referenced, or its runs become `missed`.
|
|
18
|
-
- Several processes may share a home
|
|
18
|
+
- Several processes may share a home: scheduled actions take no home lock, only the short per-action mutex (`<id>/.mutex`); the one-process lock is on session folders only. Ownership: a process only starts, delivers or recovers actions whose Agent it registered. An action nobody here owns is left alone until its delivery time passed with no run started, then marked `missed` (`no_agent`).
|
|
19
19
|
- Exactly once: a scheduled start re-checks under the action's mutex that it is still `waiting` and due; `markMissed` only changes the state the caller saw; a delivery is emitted only by the process that removed its entry from `delivery.json`.
|
|
20
20
|
- A run in flight keeps `<id>/.heartbeat` (pid, host, token; mtime refreshed every 5 s, stale after 60 s), written before the record shows `cooking`. A cooking action whose owner is dead (pid gone, stale, or none) is recovered by a live process that owns its Agent, at start and every 15 s. `delete` of a run another live process is cooking is refused, even with `force`.
|
|
21
21
|
- Times come from `Date`, local clock only. Tests inject `scheduler.now` / `tickMs` and call `tick()`.
|
|
@@ -12,8 +12,8 @@ Small shared helpers. Each file has one job.
|
|
|
12
12
|
| `safe-io.ts` | `fileAccess.roots` containment and the guarded remote fetch (private hosts refused, redirects re-checked, size cap). |
|
|
13
13
|
| `raw-capture.ts` | The `captureRaw` policy for `AgentResponse.raw`. |
|
|
14
14
|
| `cache.ts` | Cache-key clamp (64 chars) and the central secret redaction. |
|
|
15
|
-
| `errors.ts` | `toConciseProviderError` and pre-network modality guards. |
|
|
16
|
-
| `retry.ts` | `withRetries`, `isTransientError
|
|
15
|
+
| `errors.ts` | `toConciseProviderError` (any raw provider failure becomes one-line `ProviderError`: `code` `"provider"`, `retryable` from status/code, the provider's code as `providerCode`; SDK errors pass through), `isTransientError`, and the pre-network modality guards (`ValidationError`). |
|
|
16
|
+
| `retry.ts` | `withRetries`, `isTransientError` (re-exported from `errors.ts`), `isRetryableRun`, `sleep`. Structured data only (status, code, flags), never message text. |
|
|
17
17
|
| `session.ts` | Id helpers (`createSessionId`, `hashSessionPart`, `createTrackingId`, run/turn ids), `parseModelRef`, `previewPrompt`. |
|
|
18
18
|
| `serialization.ts` | `toJsonSafe`, `safeStringify`, `escapeXml`. |
|
|
19
19
|
| `base64.ts`, `thought-signature.ts` | Runtime-agnostic base64; Gemini thought-signature helpers. |
|
package/src/utils/errors.ts
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
import type { ProviderContext } from "../types/message.ts";
|
|
2
2
|
import { getModelFromCatalog } from "../models/catalog.ts";
|
|
3
|
+
import { AgentAccelError, ProviderError, ValidationError, isAbortError } from "../types/errors.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Checks whether a provider error is transient and safe to retry.
|
|
7
|
+
* Structured-data only: status / code / transient flag. Never message text,
|
|
8
|
+
* so a 400 mentioning "512 tokens" is not retried.
|
|
9
|
+
*/
|
|
10
|
+
export function isTransientError(error: unknown): boolean {
|
|
11
|
+
if (!error) return false;
|
|
12
|
+
if (isAbortError(error)) return false;
|
|
13
|
+
const err = error as Record<string, unknown>;
|
|
14
|
+
if (err["transient"] === true) return true;
|
|
15
|
+
if ((err as { retryable?: unknown }).retryable === true) return true;
|
|
16
|
+
const status = Number(
|
|
17
|
+
(err["status"] as number | undefined) ??
|
|
18
|
+
(err["statusCode"] as number | undefined) ??
|
|
19
|
+
((err["response"] as Record<string, unknown> | undefined)?.["status"] as number | undefined) ??
|
|
20
|
+
((err["lastError"] as Record<string, unknown> | undefined)?.["status"] as number | undefined) ??
|
|
21
|
+
NaN
|
|
22
|
+
);
|
|
23
|
+
if ([408, 425, 429, 500, 502, 503, 504].includes(status)) return true;
|
|
24
|
+
const code = String((err["providerCode"] as string | undefined) ?? (err["code"] as string | undefined) ?? "").toUpperCase();
|
|
25
|
+
if (["ECONNRESET", "ECONNREFUSED", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT", "EPIPE", "ETIMEDOUT"].includes(code)) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
3
30
|
|
|
4
31
|
const CONCISE = "__agentAccelConcise";
|
|
5
32
|
|
|
@@ -49,12 +76,12 @@ function displayModel(providerId: string, modelId: string): string {
|
|
|
49
76
|
}
|
|
50
77
|
|
|
51
78
|
/**
|
|
52
|
-
* Collapses a raw provider error (which carries the full request/response
|
|
53
|
-
*
|
|
54
|
-
* available but non-enumerable, so runtime dumps stay small.
|
|
79
|
+
* Collapses a raw provider error (which carries the full request/response dump as enumerable props) into a one-line
|
|
80
|
+
* `ProviderError` with `retryable` set from its status/code; the provider's own error code is `providerCode`. Raw
|
|
81
|
+
* details stay available but non-enumerable, so runtime dumps stay small. An SDK error (already typed) passes through.
|
|
55
82
|
*/
|
|
56
83
|
export function toConciseProviderError(error: unknown, providerId: string, modelId: string): Error {
|
|
57
|
-
if (error instanceof Error && (error as any)[CONCISE]) return error;
|
|
84
|
+
if (error instanceof AgentAccelError || (error instanceof Error && (error as any)[CONCISE])) return error;
|
|
58
85
|
const err = error as any;
|
|
59
86
|
const status = readStatus(err);
|
|
60
87
|
const url = readUrl(err);
|
|
@@ -70,10 +97,10 @@ export function toConciseProviderError(error: unknown, providerId: string, model
|
|
|
70
97
|
if (rawMeta && !detail.includes(rawMeta.slice(0, 80))) {
|
|
71
98
|
detail = `${detail}\n${rawMeta.slice(0, 500)}`.slice(0, 800);
|
|
72
99
|
}
|
|
73
|
-
const concise = new
|
|
74
|
-
`[${displayModel(providerId, modelId)}] request failed${status ? ` (${status})` : ""}: ${detail}
|
|
100
|
+
const concise = new ProviderError(
|
|
101
|
+
`[${displayModel(providerId, modelId)}] request failed${status ? ` (${status})` : ""}: ${detail}`,
|
|
102
|
+
{ status, provider: providerId, model: modelId, retryable: isTransientError(err) }
|
|
75
103
|
);
|
|
76
|
-
concise.name = "AgentAccelProviderError";
|
|
77
104
|
Object.defineProperties(concise, {
|
|
78
105
|
[CONCISE]: { value: true, enumerable: false },
|
|
79
106
|
statusCode: { value: status, enumerable: false },
|
|
@@ -90,11 +117,10 @@ export function toConciseProviderError(error: unknown, providerId: string, model
|
|
|
90
117
|
...(typeof err?.errorType === "string"
|
|
91
118
|
? { errorType: { value: err.errorType, enumerable: false } }
|
|
92
119
|
: {}),
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
// so chain-expiry detectors never lose it.
|
|
120
|
+
// The provider's own error code (e.g. Google's `not_found` chain-eviction code, which carries no HTTP status on
|
|
121
|
+
// SSE error frames); `code` itself is the SDK kind (`"provider"`). Survives re-wraps so chain detectors never lose it.
|
|
96
122
|
...((typeof err?.code === "string" || typeof err?.code === "number") && err.code !== ""
|
|
97
|
-
? {
|
|
123
|
+
? { providerCode: { value: err.code, enumerable: false } }
|
|
98
124
|
: {}),
|
|
99
125
|
// Server rate-limit hint in ms (from Retry-After); survives re-wraps so
|
|
100
126
|
// `withRetries` can honor it instead of hammering a 429.
|
|
@@ -120,7 +146,7 @@ const PART_TO_MODALITY: Record<string, "image" | "audio" | "video" | "pdf"> = {
|
|
|
120
146
|
* lacks a requested input modality (image/audio/video/document; files are
|
|
121
147
|
* not checked on OpenRouter, which parses them for any model). Unknown
|
|
122
148
|
* models (custom providers, dynamic routers) are skipped: the provider
|
|
123
|
-
* verdict stands and surfaces via toConciseProviderError instead.
|
|
149
|
+
* verdict stands and surfaces via toConciseProviderError instead. Throws a `ValidationError`.
|
|
124
150
|
*/
|
|
125
151
|
export function assertModalitiesSupported(
|
|
126
152
|
context: ProviderContext,
|
|
@@ -147,16 +173,10 @@ export function assertModalitiesSupported(
|
|
|
147
173
|
if (!supported) return;
|
|
148
174
|
const missing = [...needed].filter((k) => !supported.includes(k));
|
|
149
175
|
if (missing.length === 0) return;
|
|
150
|
-
|
|
151
|
-
`[${displayModel(providerId, modelId)}] unsupported ${missing.join("+")} input (supports: ${supported.join(", ") || "text"}). Use a capable model or drop the ${missing.join("+")} part
|
|
176
|
+
throw new ValidationError(
|
|
177
|
+
`[${displayModel(providerId, modelId)}] unsupported ${missing.join("+")} input (supports: ${supported.join(", ") || "text"}). Use a capable model or drop the ${missing.join("+")} part.`,
|
|
178
|
+
{ provider: providerId, model: modelId }
|
|
152
179
|
);
|
|
153
|
-
err.name = "AgentAccelProviderError";
|
|
154
|
-
Object.defineProperties(err, {
|
|
155
|
-
[CONCISE]: { value: true, enumerable: false },
|
|
156
|
-
provider: { value: providerId, enumerable: false },
|
|
157
|
-
model: { value: modelId, enumerable: false },
|
|
158
|
-
});
|
|
159
|
-
throw err;
|
|
160
180
|
}
|
|
161
181
|
|
|
162
182
|
/**
|
|
@@ -169,7 +189,7 @@ export function assertModalitiesSupported(
|
|
|
169
189
|
* `openrouter/stealth/space-bunny-alpha` under the old Responses skin despite
|
|
170
190
|
* its catalog `video` flag, which is chat-transport oriented). Fail fast with
|
|
171
191
|
* a clear one-liner instead; video stays Gemini-only, and OpenRouter Chat
|
|
172
|
-
* Completions carries `video_url` natively (no guard there).
|
|
192
|
+
* Completions carries `video_url` natively (no guard there). Throws a `ValidationError`.
|
|
173
193
|
*/
|
|
174
194
|
export function assertNoVideoPartsOnResponses(
|
|
175
195
|
context: ProviderContext,
|
|
@@ -197,14 +217,8 @@ export function assertNoVideoPartsOnResponses(
|
|
|
197
217
|
// contradict the rejection).
|
|
198
218
|
if (known && known.length > 0) supported = known.filter((m) => m !== "video").join(", ") || "text";
|
|
199
219
|
} catch {}
|
|
200
|
-
|
|
201
|
-
`[${displayModel(providerId, modelId)}] unsupported video input (the Responses API accepts text/image/file only; supports: ${supported}). Use a video-capable model (e.g. google/gemini-*) or drop the video part
|
|
220
|
+
throw new ValidationError(
|
|
221
|
+
`[${displayModel(providerId, modelId)}] unsupported video input (the Responses API accepts text/image/file only; supports: ${supported}). Use a video-capable model (e.g. google/gemini-*) or drop the video part.`,
|
|
222
|
+
{ provider: providerId, model: modelId }
|
|
202
223
|
);
|
|
203
|
-
err.name = "AgentAccelProviderError";
|
|
204
|
-
Object.defineProperties(err, {
|
|
205
|
-
[CONCISE]: { value: true, enumerable: false },
|
|
206
|
-
provider: { value: providerId, enumerable: false },
|
|
207
|
-
model: { value: modelId, enumerable: false },
|
|
208
|
-
});
|
|
209
|
-
throw err;
|
|
210
224
|
}
|
package/src/utils/headers.ts
CHANGED
|
@@ -24,7 +24,7 @@ export function getPackageVersion(): string {
|
|
|
24
24
|
const v = (globalThis as any).process?.env?.npm_package_version;
|
|
25
25
|
if (typeof v === "string" && v) return v;
|
|
26
26
|
} catch {}
|
|
27
|
-
return "0.5.
|
|
27
|
+
return "0.5.9";
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
// User-Agent string with version and platform; in: AGENT_ACCEL_USER_AGENT, out: a string.
|
package/src/utils/retry.ts
CHANGED
|
@@ -1,33 +1,7 @@
|
|
|
1
|
-
import { toConciseProviderError } from "./errors.ts";
|
|
1
|
+
import { toConciseProviderError, isTransientError } from "./errors.ts";
|
|
2
2
|
import { isAbortError } from "../types/errors.ts";
|
|
3
3
|
|
|
4
|
-
export { isAbortError };
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Checks whether a provider error is transient and safe to retry.
|
|
8
|
-
* Structured-data only: status / code / transient flag. Never message text,
|
|
9
|
-
* so a 400 mentioning "512 tokens" is not retried.
|
|
10
|
-
*/
|
|
11
|
-
export function isTransientError(error: unknown): boolean {
|
|
12
|
-
if (!error) return false;
|
|
13
|
-
if (isAbortError(error)) return false;
|
|
14
|
-
const err = error as Record<string, unknown>;
|
|
15
|
-
if (err["transient"] === true) return true;
|
|
16
|
-
if ((err as { retryable?: unknown }).retryable === true) return true;
|
|
17
|
-
const status = Number(
|
|
18
|
-
(err["status"] as number | undefined) ??
|
|
19
|
-
(err["statusCode"] as number | undefined) ??
|
|
20
|
-
((err["response"] as Record<string, unknown> | undefined)?.["status"] as number | undefined) ??
|
|
21
|
-
((err["lastError"] as Record<string, unknown> | undefined)?.["status"] as number | undefined) ??
|
|
22
|
-
NaN
|
|
23
|
-
);
|
|
24
|
-
if ([408, 425, 429, 500, 502, 503, 504].includes(status)) return true;
|
|
25
|
-
const code = String((err["code"] as string | undefined) ?? "").toUpperCase();
|
|
26
|
-
if (["ECONNRESET", "ECONNREFUSED", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT", "EPIPE", "ETIMEDOUT"].includes(code)) {
|
|
27
|
-
return true;
|
|
28
|
-
}
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
4
|
+
export { isAbortError, isTransientError };
|
|
31
5
|
|
|
32
6
|
/**
|
|
33
7
|
* Whether a FAILED RUN (the provider layer already retried the call) is worth trying again: the failure is transient
|