runward 0.6.0
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/LICENSE +21 -0
- package/NOTICE.md +13 -0
- package/README.md +128 -0
- package/dist/cli.js +76 -0
- package/dist/commands/check.js +50 -0
- package/dist/commands/doctor.js +75 -0
- package/dist/commands/init.js +90 -0
- package/dist/commands/status.js +49 -0
- package/dist/commands/update.js +48 -0
- package/dist/lib/constants.js +2 -0
- package/dist/lib/mission.js +96 -0
- package/dist/lib/paths.js +28 -0
- package/dist/lib/styles.js +60 -0
- package/dist/lib/tools.js +59 -0
- package/dist/lib/write.js +30 -0
- package/package.json +29 -0
- package/templates/mission/adr/ADR-0000-template.md +38 -0
- package/templates/mission/architecture.md +60 -0
- package/templates/mission/decision-matrix.md +37 -0
- package/templates/mission/evaluation-rubric.md +60 -0
- package/templates/mission/floor.md +47 -0
- package/templates/mission/framing.md +58 -0
- package/templates/mission/mission-contract.md +58 -0
- package/templates/mission/observability-schema.md +53 -0
- package/templates/mission/port-contract.md +63 -0
- package/templates/mission/reference-stack.md +70 -0
- package/templates/mission/runbook.md +70 -0
- package/templates/mission/shared-bricks.md +74 -0
- package/templates/mission/threat-model.md +53 -0
- package/templates/rules/async-job-guardrails.md +95 -0
- package/templates/rules/async-post-turn-pipeline.md +96 -0
- package/templates/rules/async-scheduled-maintenance.md +95 -0
- package/templates/rules/cache-three-tier-architecture.md +44 -0
- package/templates/rules/checklist-day-zero-project.md +97 -0
- package/templates/rules/checklist-pre-production-observability.md +99 -0
- package/templates/rules/checklist-pre-production-performance.md +109 -0
- package/templates/rules/checklist-pre-production-resilience.md +99 -0
- package/templates/rules/checklist-pre-production-security.md +86 -0
- package/templates/rules/config-secrets-boundary.md +57 -0
- package/templates/rules/config-typing-zod.md +78 -0
- package/templates/rules/contracts-governance.md +64 -0
- package/templates/rules/data-memory-consolidation.md +81 -0
- package/templates/rules/data-memory-invalidation.md +90 -0
- package/templates/rules/data-memory-scoring.md +154 -0
- package/templates/rules/data-migrations-forward-only.md +55 -0
- package/templates/rules/data-orphan-cleanup.md +142 -0
- package/templates/rules/data-ttl-types.md +100 -0
- package/templates/rules/eval-loop.md +52 -0
- package/templates/rules/frontier-deterministic-boundary.md +91 -0
- package/templates/rules/hexa-adapter-pattern.md +85 -0
- package/templates/rules/hexa-architecture.md +84 -0
- package/templates/rules/hexa-move-deterministic-out.md +93 -0
- package/templates/rules/hexa-recommended-stack.md +33 -0
- package/templates/rules/hexa-typescript-native.md +80 -0
- package/templates/rules/observability-alert-configuration.md +160 -0
- package/templates/rules/observability-llm-metrics.md +150 -0
- package/templates/rules/observability-startup-provider-log.md +128 -0
- package/templates/rules/observability-structured-json-logs.md +138 -0
- package/templates/rules/patterns-memory-router-tiered.md +139 -0
- package/templates/rules/patterns-prompt-compiler.md +126 -0
- package/templates/rules/patterns-request-id-propagation.md +137 -0
- package/templates/rules/process-adr-and-journal.md +50 -0
- package/templates/rules/provider-llm-auto-detection.md +54 -0
- package/templates/rules/provider-no-crash-missing-env.md +67 -0
- package/templates/rules/quality-codebase-metrics.md +115 -0
- package/templates/rules/quality-zod-input-validation.md +131 -0
- package/templates/rules/resilience-fail-open.md +65 -0
- package/templates/rules/resilience-multi-provider-fallback.md +100 -0
- package/templates/rules/resilience-retry-backoff.md +115 -0
- package/templates/rules/resilience-retryable-errors.md +107 -0
- package/templates/rules/routing-confidence-upgrade.md +89 -0
- package/templates/rules/routing-model-cost-ratios.md +68 -0
- package/templates/rules/routing-smart-complexity.md +131 -0
- package/templates/rules/scaling-db-connection-pooling.md +121 -0
- package/templates/rules/scaling-distributed-rate-limiting.md +141 -0
- package/templates/rules/scaling-state-externalization.md +84 -0
- package/templates/rules/security-prompt-injection.md +63 -0
- package/templates/rules/state-event-sourcing.md +66 -0
- package/templates/rules/tools-registry-pattern.md +153 -0
- package/templates/rules/tools-scope-atomicity.md +103 -0
- package/templates/targets/AGENTS.md +32 -0
- package/templates/workflows/architect.md +50 -0
- package/templates/workflows/brownfield.md +52 -0
- package/templates/workflows/decision-loop.md +52 -0
- package/templates/workflows/floor.md +60 -0
- package/templates/workflows/frame.md +69 -0
- package/templates/workflows/govern.md +55 -0
- package/templates/workflows/handover.md +55 -0
- package/templates/workflows/iterate.md +52 -0
- package/templates/workflows/method.md +53 -0
- package/templates/workflows/review.md +59 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Zod Input Validation
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: Prevents invalid data from entering the system and provides type safety
|
|
5
|
+
tags: [quality, validation, security, typescript]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Zod Input Validation
|
|
9
|
+
|
|
10
|
+
Validate all external inputs with Zod at system boundaries.
|
|
11
|
+
|
|
12
|
+
**Where to Validate:**
|
|
13
|
+
|
|
14
|
+
- API endpoints (request body, query params)
|
|
15
|
+
- Environment variables
|
|
16
|
+
- User uploads
|
|
17
|
+
- External API responses
|
|
18
|
+
- Database query results (optional, for defense in depth)
|
|
19
|
+
|
|
20
|
+
**Incorrect:**
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// No validation - trusts external input
|
|
24
|
+
app.post('/api/projects', async (req, res) => {
|
|
25
|
+
const { name, description, budget } = req.body; // BAD: Could be anything
|
|
26
|
+
await db.projects.create({ data: { name, description, budget } });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Type assertion without validation
|
|
30
|
+
const config = process.env as Config; // BAD: Not validated
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Correct:**
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { z } from 'zod';
|
|
37
|
+
|
|
38
|
+
// Define schemas
|
|
39
|
+
const CreateProjectSchema = z.object({
|
|
40
|
+
name: z.string().min(1).max(100),
|
|
41
|
+
description: z.string().max(1000).optional(),
|
|
42
|
+
budget: z.number().positive().optional(),
|
|
43
|
+
tags: z.array(z.string()).max(10).optional(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
type CreateProjectInput = z.infer<typeof CreateProjectSchema>;
|
|
47
|
+
|
|
48
|
+
// Validate at API boundary
|
|
49
|
+
app.post('/api/projects', async (req, res) => {
|
|
50
|
+
// Parse throws ZodError if invalid
|
|
51
|
+
const input = CreateProjectSchema.parse(req.body);
|
|
52
|
+
|
|
53
|
+
// input is now typed and validated
|
|
54
|
+
await db.projects.create({ data: input });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Or with safe parsing
|
|
58
|
+
app.post('/api/projects', async (req, res) => {
|
|
59
|
+
const result = CreateProjectSchema.safeParse(req.body);
|
|
60
|
+
|
|
61
|
+
if (!result.success) {
|
|
62
|
+
return res.status(400).json({
|
|
63
|
+
error: 'Validation failed',
|
|
64
|
+
details: result.error.flatten(),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await db.projects.create({ data: result.data });
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Environment Variables:**
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// src/lib/env.ts
|
|
76
|
+
const EnvSchema = z.object({
|
|
77
|
+
DATABASE_URL: z.string().url(),
|
|
78
|
+
MODEL_API_KEY: z.string().optional(),
|
|
79
|
+
EMBEDDING_API_KEY: z.string().optional(),
|
|
80
|
+
PORT: z.coerce.number().default(3000),
|
|
81
|
+
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Validate once at startup
|
|
85
|
+
export const env = EnvSchema.parse(process.env);
|
|
86
|
+
|
|
87
|
+
// Usage: env.DATABASE_URL is typed and validated
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**API Response Validation:**
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// Validate external API responses
|
|
94
|
+
const ExternalAPIResponseSchema = z.object({
|
|
95
|
+
data: z.array(z.object({
|
|
96
|
+
id: z.string(),
|
|
97
|
+
name: z.string(),
|
|
98
|
+
value: z.number(),
|
|
99
|
+
})),
|
|
100
|
+
meta: z.object({
|
|
101
|
+
page: z.number(),
|
|
102
|
+
total: z.number(),
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
async function fetchExternalData(): Promise<ExternalAPIResponse> {
|
|
107
|
+
const response = await fetch('https://api.external.com/data');
|
|
108
|
+
const json = await response.json();
|
|
109
|
+
|
|
110
|
+
// Validate response matches expected shape
|
|
111
|
+
return ExternalAPIResponseSchema.parse(json);
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Error Handling:**
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// Middleware for Zod error handling
|
|
119
|
+
function zodErrorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
|
|
120
|
+
if (err instanceof z.ZodError) {
|
|
121
|
+
return res.status(400).json({
|
|
122
|
+
error: 'Validation failed',
|
|
123
|
+
details: err.errors.map(e => ({
|
|
124
|
+
path: e.path.join('.'),
|
|
125
|
+
message: e.message,
|
|
126
|
+
})),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
next(err);
|
|
130
|
+
}
|
|
131
|
+
```
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Fail-Open for the Non-Critical, Fail-Closed for the Sensitive
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
impactDescription: Degrades gracefully where safe and denies safely where it matters, instead of applying one failure policy everywhere
|
|
5
|
+
tags: [resilience, error-handling, availability, safety]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Fail-Open for the Non-Critical, Fail-Closed for the Sensitive
|
|
9
|
+
|
|
10
|
+
> **A non-critical service down is not the application down. A sensitive action in doubt is a denied action, never a guessed one.**
|
|
11
|
+
|
|
12
|
+
The failure policy is not global. You choose it per dependency, by criticality. Getting this split wrong in either direction is a bug: fail-closed on a cache makes you brittle; fail-open on an authorisation makes you unsafe.
|
|
13
|
+
|
|
14
|
+
**Fail-open (degrade and continue) for reads and the non-critical:**
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
// Cache failure means slower, not broken.
|
|
18
|
+
async function getCached<T>(key: string): Promise<T | null> {
|
|
19
|
+
try {
|
|
20
|
+
return await cache.get<T>(key);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
log.error("[CACHE] get failed, continuing without cache", { error });
|
|
23
|
+
return null; // fail-open: continue degraded
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Fail-closed (deny, explicitly) for the sensitive:**
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// Authorisation, the grounding guard, a payment: doubt means deny.
|
|
32
|
+
async function authorise(req: Request): Promise<void> {
|
|
33
|
+
try {
|
|
34
|
+
const ok = await authz.check(req);
|
|
35
|
+
if (!ok) throw new Forbidden();
|
|
36
|
+
} catch (error) {
|
|
37
|
+
// Never "allow on error". A guard you fail open is not a guard.
|
|
38
|
+
throw new Forbidden(); // fail-closed: deny on doubt
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Suspend, do not block.** Fail-closed does not mean freezing the process. A sensitive action that needs a human serialises the agent's state and rehydrates it on the decision; low-urgency approvals are batched in a prioritised queue. You get the safety of fail-closed without a frozen agent.
|
|
44
|
+
|
|
45
|
+
**Service classification:**
|
|
46
|
+
|
|
47
|
+
| Service / action | Policy | On failure |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| Database (read of record) | Critical | Error to the user |
|
|
50
|
+
| Model call | Critical | Error after retries |
|
|
51
|
+
| Cache | Non-critical | Continue, slower (fail-open) |
|
|
52
|
+
| Audit / analytics logging | Non-critical | Log locally, continue (fail-open) |
|
|
53
|
+
| Optional enrichment (search, memory) | Non-critical | Skip, continue (fail-open) |
|
|
54
|
+
| Authorisation / tenant isolation | Sensitive | Deny (fail-closed) |
|
|
55
|
+
| Grounding / safety guard | Sensitive | Reject the response (fail-closed) |
|
|
56
|
+
| Payment, irreversible write | Sensitive | Abort + require confirmation (fail-closed) |
|
|
57
|
+
|
|
58
|
+
**The test that tells them apart:** ask "if this dependency is wrong or absent, is the safe answer *continue* or *deny*?" Continue -> fail-open. Deny -> fail-closed. Anything touching safety, authorisation, money, or the truth of a served figure is fail-closed.
|
|
59
|
+
|
|
60
|
+
**Checklist:**
|
|
61
|
+
|
|
62
|
+
- [ ] Every external dependency has an explicit failure policy, chosen by criticality.
|
|
63
|
+
- [ ] No safety/authorisation/guard path fails open.
|
|
64
|
+
- [ ] No read-side enrichment fails closed and takes the request down with it.
|
|
65
|
+
- [ ] Fail-closed sensitive actions suspend-and-rehydrate rather than freeze.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Multi-Provider Fallback
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Ensures LLM availability by falling back to alternative providers
|
|
5
|
+
tags: [resilience, llm, providers, availability]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Multi-Provider Fallback
|
|
9
|
+
|
|
10
|
+
Configure fallback providers to handle outages. If primary provider fails, automatically try secondary.
|
|
11
|
+
|
|
12
|
+
**Model gateway configuration (one option):**
|
|
13
|
+
|
|
14
|
+
```yaml
|
|
15
|
+
# gateway_config.yaml
|
|
16
|
+
model_list:
|
|
17
|
+
- model_name: app-fast
|
|
18
|
+
gateway_params:
|
|
19
|
+
model: small-fast-model
|
|
20
|
+
fallbacks:
|
|
21
|
+
- alt-small-model
|
|
22
|
+
|
|
23
|
+
- model_name: app-balanced
|
|
24
|
+
gateway_params:
|
|
25
|
+
model: mid-tier-model
|
|
26
|
+
fallbacks:
|
|
27
|
+
- alt-mid-model
|
|
28
|
+
|
|
29
|
+
- model_name: app-deep
|
|
30
|
+
gateway_params:
|
|
31
|
+
model: top-tier-model
|
|
32
|
+
fallbacks:
|
|
33
|
+
- alt-mid-model # No Deep equivalent
|
|
34
|
+
|
|
35
|
+
router_settings:
|
|
36
|
+
retry_policy:
|
|
37
|
+
num_retries: 3
|
|
38
|
+
backoff_factor: 2
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Manual Fallback (without a model gateway):**
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// src/lib/providers/llm-client.ts
|
|
45
|
+
interface LLMClient {
|
|
46
|
+
complete(request: CompletionRequest): Promise<CompletionResponse>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const providers: LLMClient[] = [
|
|
50
|
+
new PrimaryModelClient(),
|
|
51
|
+
new SecondaryModelClient(),
|
|
52
|
+
new TertiaryModelClient(),
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
async function completeWithFallback(
|
|
56
|
+
request: CompletionRequest
|
|
57
|
+
): Promise<CompletionResponse> {
|
|
58
|
+
let lastError: Error;
|
|
59
|
+
|
|
60
|
+
for (const provider of providers) {
|
|
61
|
+
try {
|
|
62
|
+
return await provider.complete(request);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
lastError = error as Error;
|
|
65
|
+
console.warn(`[LLM] Provider failed, trying next:`, {
|
|
66
|
+
provider: provider.constructor.name,
|
|
67
|
+
error: lastError.message,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
throw new Error(`All LLM providers failed. Last error: ${lastError!.message}`);
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Provider Priority:**
|
|
77
|
+
|
|
78
|
+
| Priority | Provider | Notes |
|
|
79
|
+
|----------|----------|-------|
|
|
80
|
+
| 1 | Primary provider | best measured quality |
|
|
81
|
+
| 2 | Secondary provider | availability fallback |
|
|
82
|
+
| 3 | Tertiary provider | last resort |
|
|
83
|
+
| 4 | Local or offline | development, air-gapped |
|
|
84
|
+
|
|
85
|
+
**When to use each approach:**
|
|
86
|
+
|
|
87
|
+
- **a model gateway**: Production, complex routing, multiple providers
|
|
88
|
+
- **Manual fallback**: Simple apps, fewer dependencies
|
|
89
|
+
- **Single provider**: Development, prototyping
|
|
90
|
+
|
|
91
|
+
**Monitoring:**
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// Track fallback usage
|
|
95
|
+
console.info('[LLM] Request completed', {
|
|
96
|
+
primaryProvider: 'primary',
|
|
97
|
+
usedProvider: response.provider, // May differ if fallback used
|
|
98
|
+
fallbackUsed: response.provider !== 'primary',
|
|
99
|
+
});
|
|
100
|
+
```
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: LLM Retry with Exponential Backoff
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Handles transient LLM failures gracefully without overwhelming the API
|
|
5
|
+
tags: [resilience, llm, retry, error-handling]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## LLM Retry with Exponential Backoff
|
|
9
|
+
|
|
10
|
+
LLM APIs experience rate limits and transient errors. Implement retry with exponential backoff.
|
|
11
|
+
|
|
12
|
+
**Incorrect:**
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
// No retry - single failure = user error
|
|
16
|
+
const response = await model.complete({...});
|
|
17
|
+
|
|
18
|
+
// Or: immediate retry flood
|
|
19
|
+
while (retries < 3) {
|
|
20
|
+
try {
|
|
21
|
+
return await model.complete({...});
|
|
22
|
+
} catch {
|
|
23
|
+
retries++; // BAD: No delay
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Correct:**
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// src/lib/resilience/retry.ts
|
|
32
|
+
interface RetryConfig {
|
|
33
|
+
maxRetries: number;
|
|
34
|
+
initialDelayMs: number;
|
|
35
|
+
maxDelayMs: number;
|
|
36
|
+
retryableErrors: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const defaultConfig: RetryConfig = {
|
|
40
|
+
maxRetries: 3,
|
|
41
|
+
initialDelayMs: 1000,
|
|
42
|
+
maxDelayMs: 10000,
|
|
43
|
+
retryableErrors: ['rate_limit_error', 'overloaded_error', 'api_error'],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function isRetryable(error: unknown, retryableErrors: string[]): boolean {
|
|
47
|
+
if (error instanceof Error) {
|
|
48
|
+
return retryableErrors.some(e => error.message.includes(e));
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function sleep(ms: number): Promise<void> {
|
|
54
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function withLLMRetry<T>(
|
|
58
|
+
fn: () => Promise<T>,
|
|
59
|
+
config = defaultConfig
|
|
60
|
+
): Promise<T> {
|
|
61
|
+
let lastError: Error;
|
|
62
|
+
|
|
63
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
64
|
+
try {
|
|
65
|
+
return await fn();
|
|
66
|
+
} catch (error) {
|
|
67
|
+
lastError = error as Error;
|
|
68
|
+
|
|
69
|
+
// Don't retry non-retryable errors
|
|
70
|
+
if (!isRetryable(error, config.retryableErrors)) {
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Don't delay after last attempt
|
|
75
|
+
if (attempt < config.maxRetries) {
|
|
76
|
+
const delay = Math.min(
|
|
77
|
+
config.initialDelayMs * Math.pow(2, attempt),
|
|
78
|
+
config.maxDelayMs
|
|
79
|
+
);
|
|
80
|
+
console.warn(`[LLM] Retry ${attempt + 1}/${config.maxRetries} after ${delay}ms`, {
|
|
81
|
+
error: lastError.message,
|
|
82
|
+
});
|
|
83
|
+
await sleep(delay);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
throw lastError!;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Usage
|
|
92
|
+
const response = await withLLMRetry(() =>
|
|
93
|
+
model.complete({
|
|
94
|
+
model: 'mid-tier-model',
|
|
95
|
+
messages,
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Retry Delays:**
|
|
101
|
+
|
|
102
|
+
| Attempt | Delay | Total Wait |
|
|
103
|
+
|---------|-------|------------|
|
|
104
|
+
| 1 | 1000ms | 1s |
|
|
105
|
+
| 2 | 2000ms | 3s |
|
|
106
|
+
| 3 | 4000ms | 7s |
|
|
107
|
+
|
|
108
|
+
**Retryable vs Non-Retryable:**
|
|
109
|
+
|
|
110
|
+
| Retryable | Non-Retryable |
|
|
111
|
+
|-----------|---------------|
|
|
112
|
+
| `rate_limit_error` | `invalid_request_error` |
|
|
113
|
+
| `overloaded_error` | `authentication_error` |
|
|
114
|
+
| `api_error` | `invalid_api_key` |
|
|
115
|
+
| Network timeouts | Validation errors |
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Retryable vs Non-Retryable Errors
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: Prevents wasted retries and ensures quick failure for unrecoverable errors
|
|
5
|
+
tags: [resilience, error-handling, llm]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Retryable vs Non-Retryable Errors
|
|
9
|
+
|
|
10
|
+
Not all errors should be retried. Distinguishing them prevents wasted time and API calls.
|
|
11
|
+
|
|
12
|
+
**Error Classification:**
|
|
13
|
+
|
|
14
|
+
| Retryable (transient) | Non-Retryable (permanent) |
|
|
15
|
+
|----------------------|---------------------------|
|
|
16
|
+
| `rate_limit_error` | `invalid_request_error` |
|
|
17
|
+
| `overloaded_error` | `authentication_error` |
|
|
18
|
+
| `api_error` (500s) | `invalid_api_key` |
|
|
19
|
+
| Network timeout | `model_not_found` |
|
|
20
|
+
| Connection reset | Validation errors |
|
|
21
|
+
| `service_unavailable` | `content_policy_violation` |
|
|
22
|
+
|
|
23
|
+
**Incorrect:**
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
// Retries everything, including permanent errors
|
|
27
|
+
async function callLLM(request: Request): Promise<Response> {
|
|
28
|
+
for (let i = 0; i < 3; i++) {
|
|
29
|
+
try {
|
|
30
|
+
return await model.complete(request);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
await sleep(1000 * Math.pow(2, i)); // BAD: Retries invalid_api_key
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
throw new Error('Failed after retries');
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Correct:**
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
const RETRYABLE_ERRORS = new Set([
|
|
43
|
+
'rate_limit_error',
|
|
44
|
+
'overloaded_error',
|
|
45
|
+
'api_error',
|
|
46
|
+
'service_unavailable',
|
|
47
|
+
'ECONNRESET',
|
|
48
|
+
'ETIMEDOUT',
|
|
49
|
+
'ENOTFOUND',
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
function isRetryable(error: unknown): boolean {
|
|
53
|
+
if (error instanceof ModelAPIError) {
|
|
54
|
+
// provider SDK specific
|
|
55
|
+
if (error.status === 429) return true; // Rate limit
|
|
56
|
+
if (error.status >= 500) return true; // Server error
|
|
57
|
+
return false; // Client errors (4xx except 429)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (error instanceof Error) {
|
|
61
|
+
// Network errors
|
|
62
|
+
return RETRYABLE_ERRORS.has(error.name) ||
|
|
63
|
+
RETRYABLE_ERRORS.has((error as any).code);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function callLLM(request: Request): Promise<Response> {
|
|
70
|
+
let lastError: Error;
|
|
71
|
+
|
|
72
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
73
|
+
try {
|
|
74
|
+
return await model.complete(request);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
lastError = error as Error;
|
|
77
|
+
|
|
78
|
+
// Fail fast on permanent errors
|
|
79
|
+
if (!isRetryable(error)) {
|
|
80
|
+
console.error('[LLM] Non-retryable error:', error);
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Retry with backoff for transient errors
|
|
85
|
+
const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
|
|
86
|
+
console.warn(`[LLM] Retryable error, waiting ${delay}ms:`, error);
|
|
87
|
+
await sleep(delay);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
throw lastError!;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**HTTP Status Code Guide:**
|
|
96
|
+
|
|
97
|
+
| Status | Meaning | Retry? |
|
|
98
|
+
|--------|---------|--------|
|
|
99
|
+
| 400 | Bad Request | No |
|
|
100
|
+
| 401 | Unauthorized | No |
|
|
101
|
+
| 403 | Forbidden | No |
|
|
102
|
+
| 404 | Not Found | No |
|
|
103
|
+
| 429 | Rate Limited | **Yes** |
|
|
104
|
+
| 500 | Server Error | **Yes** |
|
|
105
|
+
| 502 | Bad Gateway | **Yes** |
|
|
106
|
+
| 503 | Unavailable | **Yes** |
|
|
107
|
+
| 504 | Timeout | **Yes** |
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: If Doubt, Upgrade the Model
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
impactDescription: Prevents quality degradation from incorrect model selection
|
|
5
|
+
tags: [routing, llm, cost-optimization, quality]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## If Doubt, Upgrade the Model
|
|
9
|
+
|
|
10
|
+
When the complexity classifier isn't confident, always upgrade to a more capable model. The cost of a wrong downgrade (poor response) exceeds the cost of unnecessary upgrade.
|
|
11
|
+
|
|
12
|
+
**The Rule:**
|
|
13
|
+
|
|
14
|
+
> **CRITICAL: If confidence < 0.6, ALWAYS upgrade to the next tier.**
|
|
15
|
+
|
|
16
|
+
**Incorrect:**
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// Uses lower model even when uncertain
|
|
20
|
+
function selectModel(complexity: ComplexityResult): Model {
|
|
21
|
+
switch (complexity.level) {
|
|
22
|
+
case 'casual': return 'fast';
|
|
23
|
+
case 'task': return 'balanced';
|
|
24
|
+
case 'deep': return 'deep';
|
|
25
|
+
}
|
|
26
|
+
// BAD: No confidence check - may misroute
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Correct:**
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
interface ComplexityResult {
|
|
34
|
+
level: 'casual' | 'task' | 'planning' | 'deep';
|
|
35
|
+
confidence: number; // 0-1
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function selectModel(complexity: ComplexityResult): Model {
|
|
39
|
+
const { level, confidence } = complexity;
|
|
40
|
+
|
|
41
|
+
// If confidence is low, upgrade to be safe
|
|
42
|
+
if (confidence < 0.6) {
|
|
43
|
+
console.info(`[ROUTING] Low confidence (${confidence}), upgrading from ${level}`);
|
|
44
|
+
return upgradeModel(level);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
switch (level) {
|
|
48
|
+
case 'casual': return 'fast';
|
|
49
|
+
case 'task':
|
|
50
|
+
case 'planning': return 'balanced';
|
|
51
|
+
case 'deep': return 'deep';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function upgradeModel(level: string): Model {
|
|
56
|
+
switch (level) {
|
|
57
|
+
case 'casual': return 'balanced'; // fast → balanced
|
|
58
|
+
case 'task': return 'balanced'; // stay balanced
|
|
59
|
+
case 'planning': return 'deep'; // balanced → deep
|
|
60
|
+
case 'deep': return 'deep'; // stay deep
|
|
61
|
+
default: return 'balanced'; // safe default
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Cost Analysis:**
|
|
67
|
+
|
|
68
|
+
| Scenario | Model | Cost | Quality Risk |
|
|
69
|
+
|----------|-------|------|--------------|
|
|
70
|
+
| Confident casual → Fast | $0.005 | Low |
|
|
71
|
+
| Uncertain casual → Fast | $0.005 | **HIGH** (wrong model) |
|
|
72
|
+
| Uncertain casual → Balanced | $0.025 | Low |
|
|
73
|
+
|
|
74
|
+
**The Math:**
|
|
75
|
+
|
|
76
|
+
- Wrong model = user frustration + retry = 2x cost minimum
|
|
77
|
+
- Upgrade cost = 5x base (Balanced vs Fast)
|
|
78
|
+
- But: upgrade only when uncertain (~30% of cases)
|
|
79
|
+
- Net: slight cost increase, major quality improvement
|
|
80
|
+
|
|
81
|
+
**Detection Flow:**
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
Message → detectWithRegex() → confidence ≥ 0.6?
|
|
85
|
+
├─ YES → Use regex result
|
|
86
|
+
└─ NO → detectWithFastModel() → confidence ≥ 0.6?
|
|
87
|
+
├─ YES → Use Fast result
|
|
88
|
+
└─ NO → UPGRADE MODEL
|
|
89
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Model Cost Ratios
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Essential reference for cost optimization decisions
|
|
5
|
+
tags: [routing, llm, cost-optimization, reference]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Model Cost Ratios
|
|
9
|
+
|
|
10
|
+
Understanding cost ratios is essential for routing decisions.
|
|
11
|
+
|
|
12
|
+
**Cost ratios (illustrative orders of magnitude, verify current vendor prices):**
|
|
13
|
+
|
|
14
|
+
| Model | Input ($/M tokens) | Output ($/M tokens) | Ratio vs Balanced |
|
|
15
|
+
|-------|-------------------|---------------------|-----------------|
|
|
16
|
+
| Fast | $0.25 | $1.25 | **0.08x** |
|
|
17
|
+
| Balanced | $3.00 | $15.00 | 1.0x |
|
|
18
|
+
| Deep | $15.00 | $75.00 | 5.0x |
|
|
19
|
+
|
|
20
|
+
**Cost per typical request:**
|
|
21
|
+
|
|
22
|
+
| Model | Typical Request Cost | Use Case |
|
|
23
|
+
|-------|---------------------|----------|
|
|
24
|
+
| Fast | ~$0.005 | Greetings, simple Q&A, confirmations |
|
|
25
|
+
| Balanced | ~$0.025 | Standard tasks, analysis, coding |
|
|
26
|
+
| Deep | ~$0.10 | Complex reasoning, architecture, deep analysis |
|
|
27
|
+
|
|
28
|
+
**Key Insight:**
|
|
29
|
+
|
|
30
|
+
> Fast is **12x cheaper** than Balanced.
|
|
31
|
+
> Using Fast for 25% of traffic saves ~$0.015/request average.
|
|
32
|
+
|
|
33
|
+
**Incorrect:**
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// No cost awareness
|
|
37
|
+
const model = 'mid-tier-model'; // Always mid-tier
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Correct:**
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
const MODEL_COSTS = {
|
|
44
|
+
'small-fast-model': { input: 0.25, output: 1.25 },
|
|
45
|
+
'mid-tier-model': { input: 3.00, output: 15.00 },
|
|
46
|
+
'top-tier-model': { input: 15.00, output: 75.00 },
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
function estimateCost(model: string, inputTokens: number, outputTokens: number): number {
|
|
50
|
+
const costs = MODEL_COSTS[model];
|
|
51
|
+
return (inputTokens * costs.input + outputTokens * costs.output) / 1_000_000;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Log costs for monitoring
|
|
55
|
+
console.info('[LLM] Request completed', {
|
|
56
|
+
model,
|
|
57
|
+
inputTokens: response.usage.input_tokens,
|
|
58
|
+
outputTokens: response.usage.output_tokens,
|
|
59
|
+
estimatedCost: estimateCost(model, response.usage.input_tokens, response.usage.output_tokens),
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Cost Optimization Levers:**
|
|
64
|
+
|
|
65
|
+
1. **Smart routing** → Use Fast when possible (-40% average)
|
|
66
|
+
2. **Cache optimization** → Reduce input tokens (-60% on cached)
|
|
67
|
+
3. **Output limits** → Constrain max_tokens where appropriate
|
|
68
|
+
4. **Prompt engineering** → Shorter prompts, same quality
|