Merge remote-tracking branch 'origin/master' into codex/provider-retry-policy
# Conflicts: # .agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md # docs/core-data-structures/llm-streaming.i18n.yaml # packages/llm/llm-retry/README.md # packages/llm/llm-retry/src/index.ts # packages/llm/llm-retry/tests/retry.spec.ts
This commit is contained in:
@@ -23,7 +23,7 @@ Replay keys every call by its calling session id (`GenerateOptions.sessionId`, s
|
||||
| `file` | string | `$DSH_SNAPSHOT_FILE` | Path to the primary (parent) `session.jsonl` fixture. Required (config or env). |
|
||||
| `overrideFile` | string | `$DSH_SNAPSHOT_OVERRIDE` | Optional path to a `ReplayEntry[]` sidecar that replaces the PRIMARY session's derived script. |
|
||||
| `childFiles` | string[] | `$DSH_SNAPSHOT_CHILD_FILES` (path-delimited) | Recorded subagent child-session logs for a nested scenario; empty for a single-session scenario. |
|
||||
| `providers` | `ReplayProviderConfig[]` | — | Optional replay-only provider and model catalog. Each model may publish `contextWindow`; configured routes dispatch through the replay adapter and never perform provider I/O. |
|
||||
| `providers` | `ReplayProviderConfig[]` | — | Optional replay-only provider and model catalog. Each provider may set `retryPolicy`, and each model may publish `contextWindow`; configured routes dispatch through the replay adapter and never perform provider I/O. |
|
||||
|
||||
```yaml
|
||||
- id: llm-replay
|
||||
@@ -32,6 +32,12 @@ Replay keys every call by its calling session id (`GenerateOptions.sessionId`, s
|
||||
providers:
|
||||
- id: deepseek
|
||||
name: DeepSeek
|
||||
retryPolicy:
|
||||
mode: normal
|
||||
backoff:
|
||||
initialDelayMs: 1
|
||||
maxDelayMs: 1
|
||||
jitterRatio: 0
|
||||
models:
|
||||
- id: deepseek-v4-flash
|
||||
contextWindow: 128000
|
||||
|
||||
@@ -11,8 +11,16 @@ import { delimiter as pathDelimiter } from 'node:path'
|
||||
import type { Context } from 'cordis'
|
||||
import { decodeStorageRecord } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import type { GenerateOptions, LlmModelContext, LlmModelInfo, LlmProviderInfo, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import { LlmAdapter, LlmError, assertNever } from '@deepseek-ai/dsh-llm'
|
||||
import type {
|
||||
GenerateOptions,
|
||||
LlmModelContext,
|
||||
LlmModelInfo,
|
||||
LlmProviderInfo,
|
||||
ResolvedRetryPolicy,
|
||||
RetryPolicyConfig,
|
||||
StreamChunk,
|
||||
} from '@deepseek-ai/dsh-llm'
|
||||
import { LlmAdapter, LlmError, assertNever, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
/**
|
||||
* One recorded model call. `throw` may replay prefix chunks before failing;
|
||||
@@ -48,6 +56,8 @@ export interface ReplayProviderConfig {
|
||||
name?: string
|
||||
/** Advisory models exposed to replay scenarios that exercise discovery. */
|
||||
models?: ReplayModelConfig[]
|
||||
/** Optional provider-owned retry policy used by assembled recovery snapshots. */
|
||||
retryPolicy?: RetryPolicyConfig
|
||||
}
|
||||
|
||||
/** Resolved plugin configuration. */
|
||||
@@ -256,6 +266,15 @@ class ReplayAdapter extends LlmAdapter {
|
||||
return { id: provider, name: configured.name ?? provider }
|
||||
}
|
||||
|
||||
override providerRetryPolicy(provider: string): ResolvedRetryPolicy | undefined {
|
||||
const configured = this.providers.get(provider)
|
||||
/* v8 ignore next -- LlmService only asks about routes registered from this same map. */
|
||||
if (configured === undefined) return super.providerRetryPolicy(provider)
|
||||
return configured.retryPolicy === undefined
|
||||
? undefined
|
||||
: resolveRetryPolicy(configured.retryPolicy, `llm-replay: provider "${provider}" retryPolicy`)
|
||||
}
|
||||
|
||||
override listModels(provider: string): Promise<readonly LlmModelInfo[]> {
|
||||
const configured = this.providers.get(provider)
|
||||
/* v8 ignore next -- LlmService only asks about routes registered from this same map. */
|
||||
|
||||
@@ -240,6 +240,11 @@ describe('installLlmReplay (through the real LlmService)', () => {
|
||||
{
|
||||
id: 'deepseek',
|
||||
name: 'DeepSeek',
|
||||
retryPolicy: {
|
||||
mode: 'normal',
|
||||
maxRetries: 2,
|
||||
backoff: { initialDelayMs: 1, maxDelayMs: 1, jitterRatio: 0 },
|
||||
},
|
||||
models: [
|
||||
{ id: 'flash', contextWindow: 128_000 },
|
||||
{ id: 'pro', name: 'Pro', description: 'Larger model' },
|
||||
@@ -262,12 +267,39 @@ describe('installLlmReplay (through the real LlmService)', () => {
|
||||
await expect(ctx.llm.resolveModelContext('deepseek', 'pro')).resolves.toBeUndefined()
|
||||
await expect(ctx.llm.resolveModelContext('deepseek', 'unlisted')).resolves.toBeUndefined()
|
||||
await expect(ctx.llm.resolveModelContext('empty', 'unlisted')).resolves.toBeUndefined()
|
||||
expect(ctx.llm.providerRetryPolicy('deepseek')).toMatchObject({
|
||||
mode: 'normal',
|
||||
maxRetries: 2,
|
||||
initialDelayMs: 1,
|
||||
maxDelayMs: 1,
|
||||
jitterRatio: 0,
|
||||
})
|
||||
expect(ctx.llm.providerRetryPolicy('empty')).toMatchObject({
|
||||
mode: 'normal',
|
||||
maxRetries: 2,
|
||||
initialDelayMs: 500,
|
||||
maxDelayMs: 10_000,
|
||||
jitterRatio: 0.1,
|
||||
})
|
||||
expect(await drain(ctx.llm.stream({ provider: 'deepseek', model: 'pro', messages: [] }))).toEqual(TEXT_CHUNKS)
|
||||
|
||||
dispose()
|
||||
expect(ctx.llm.listProviders()).toEqual([])
|
||||
})
|
||||
|
||||
it('rejects an invalid replay-provider retry policy during registration', async () => {
|
||||
writeLog(TEXT_CHUNKS)
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
|
||||
expect(() => {
|
||||
installLlmReplay(ctx, {
|
||||
file,
|
||||
providers: [{ id: 'deepseek', retryPolicy: { mode: 'normal', maxRetries: -1 } }],
|
||||
})
|
||||
}).toThrow(/llm-replay: provider "deepseek" retryPolicy\.maxRetries/)
|
||||
})
|
||||
|
||||
it('serves the Nth call the Nth derived entry (positional)', async () => {
|
||||
const second: StreamChunk[] = [
|
||||
{ type: 'block-start', index: 0, blockType: 'text' },
|
||||
|
||||
Reference in New Issue
Block a user