Merge remote-tracking branch 'origin/master' into feat/read-image-context

# Conflicts:
#	docs/config-catalog.i18n.yaml
#	docs/config-catalog.md
#	docs/config-catalog.zh.md
#	packages/support/llm-replay/src/index.ts
#	packages/support/llm-replay/tests/llm-replay.spec.ts
This commit is contained in:
creatixchu
2026-08-10 20:25:25 +08:00
43 changed files with 1032 additions and 130 deletions

View File

@@ -25,7 +25,7 @@ import type {
StreamChunk,
TokenUsage,
} from '@deepseek-ai/dsh-llm'
import { LlmAdapter, LlmError, assertNever, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
import { LlmAdapter, LlmError, ReasoningEffortId, assertNever, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
/**
* One recorded model call. `throw` may replay prefix chunks before failing;
@@ -54,6 +54,18 @@ export interface ReplayModelConfig {
contextWindow?: number
/** Optional declared input modalities, so a scenario can exercise capability gates (e.g. image-capable `read_image`). */
inputModalities?: readonly ModelModality[]
/**
* Optional per-request output cap the replay route materializes when callers
* omit one, so replay reconstructs the request header a live catalog produced.
*/
defaultMaxTokens?: number
/** Optional reasoning-effort ids the replay route accepts, in display order. */
reasoningEfforts?: string[]
/**
* Optional effort materialized when callers omit one; must appear in
* {@link reasoningEfforts} or call resolution rejects the route.
*/
defaultReasoningEffort?: string
}
/** One provider route exposed by the replay adapter. */
@@ -592,6 +604,19 @@ class ReplayAdapter extends LlmAdapter {
...configuredModel?.contextWindow === undefined
? {}
: { context: { contextWindow: configuredModel.contextWindow } },
...configuredModel?.defaultMaxTokens === undefined
? {}
: { defaultMaxTokens: configuredModel.defaultMaxTokens },
...configuredModel?.reasoningEfforts === undefined
? {}
: {
reasoning: {
efforts: configuredModel.reasoningEfforts.map(id => ({ id: ReasoningEffortId(id), name: id })),
...configuredModel.defaultReasoningEffort === undefined
? {}
: { defaultEffort: ReasoningEffortId(configuredModel.defaultReasoningEffort) },
},
},
})
}

View File

@@ -593,8 +593,15 @@ describe('installLlmReplay (through the real LlmService)', () => {
backoff: { initialDelayMs: 1, maxDelayMs: 1, jitterRatio: 0 },
},
models: [
{ id: 'flash', contextWindow: 128_000, inputModalities: ['text', 'image'] },
{ id: 'pro', name: 'Pro', description: 'Larger model' },
{
id: 'flash',
contextWindow: 128_000,
inputModalities: ['text', 'image'],
defaultMaxTokens: 64_000,
reasoningEfforts: ['off', 'max'],
defaultReasoningEffort: 'max',
},
{ id: 'pro', name: 'Pro', description: 'Larger model', reasoningEfforts: ['high'] },
],
},
{ id: 'empty' },
@@ -613,9 +620,19 @@ describe('installLlmReplay (through the real LlmService)', () => {
await expect(ctx.llm.resolveModelInfo('deepseek', 'flash')).resolves.toMatchObject({
context: { contextWindow: 128_000 },
inputModalities: ['text', 'image'],
defaultMaxTokens: 64_000,
reasoning: {
efforts: [{ id: 'off', name: 'off' }, { id: 'max', name: 'max' }],
defaultEffort: 'max',
},
})
await expect(ctx.llm.resolveModelInfo('deepseek', 'pro')).resolves.not.toHaveProperty('inputModalities')
await expect(ctx.llm.resolveModelInfo('deepseek', 'pro')).resolves.not.toHaveProperty('context')
// Efforts without a configured default preserve the provider's own default.
await expect(ctx.llm.resolveModelInfo('deepseek', 'pro')).resolves.toMatchObject({
reasoning: { efforts: [{ id: 'high', name: 'high' }] },
})
await expect(ctx.llm.resolveModelInfo('deepseek', 'pro')).resolves.not.toHaveProperty('defaultMaxTokens')
await expect(ctx.llm.resolveModelInfo('deepseek', 'unlisted')).resolves.not.toHaveProperty('context')
await expect(ctx.llm.resolveModelInfo('empty', 'unlisted')).resolves.not.toHaveProperty('context')
expect(ctx.llm.providerRetryPolicy('deepseek')).toMatchObject({