Просмотр исходного кода

fix(llm-replay): publish recorded model capacity

Tianyi Cui 2 месяцев назад
Родитель
Сommit
39c9bf3ef2

+ 5 - 1
examples/tui-agent/tests/fixtures/tui-scripted-llm.ts

@@ -1,5 +1,5 @@
 import type { Context } from 'cordis'
-import type { GenerateOptions, LlmModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm'
+import type { GenerateOptions, LlmModelContext, LlmModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm'
 import { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
 
 const CONTROL_PROBE = '\u001b]2;MODEL_CONTROLLED\u0007\u001b[999CMODEL_CURSOR\u009b31mMODEL_C1'
@@ -25,6 +25,10 @@ class ScriptedTuiAdapter extends LlmAdapter {
     ])
   }
 
+  override resolveModelContext(_provider: string, _model: string): Promise<LlmModelContext> {
+    return Promise.resolve({ contextWindow: 128_000 })
+  }
+
   override async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
     if (options.model !== 'tui-scripted-model-pro' || !options.system?.includes('tui-scripted-model-pro')) {
       throw new Error('the scripted TUI request did not apply the selected model to routing and prompt variables')

+ 1 - 1
examples/tui-agent/tests/tui.snapshot.ts

@@ -33,7 +33,7 @@ import { HeadlessTerminal } from '../../../packages/ui/tui/tests/headless-termin
 const SNAPSHOTS_DIR = join(dirname(fileURLToPath(import.meta.url)), 'snapshots')
 // Keep pre-normalization layout widths identical across macOS and Linux.
 const SNAPSHOT_TMP_ROOT = process.platform === 'win32' ? tmpdir() : '/tmp'
-const PROVIDERS = [{ id: 'deepseek', models: [{ id: 'deepseek-v4-flash' }] }]
+const PROVIDERS = [{ id: 'deepseek', models: [{ id: 'deepseek-v4-flash', contextWindow: 128_000 }] }]
 const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi
 
 type SnapshotMode = 'replay' | 'record' | 'refresh'

+ 2 - 1
packages/support/llm-replay/README.md

@@ -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. Configured routes dispatch through the replay adapter and never perform provider I/O. |
+| `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. |
 
 ```yaml
 - id: llm-replay
@@ -34,6 +34,7 @@ Replay keys every call by its calling session id (`GenerateOptions.sessionId`, s
         name: DeepSeek
         models:
           - id: deepseek-v4-flash
+            contextWindow: 128000
           - id: deepseek-v4-pro
   # file/overrideFile/childFiles default to $DSH_SNAPSHOT_FILE /
   # $DSH_SNAPSHOT_OVERRIDE / $DSH_SNAPSHOT_CHILD_FILES, set by the snapshot

+ 11 - 1
packages/support/llm-replay/src/index.ts

@@ -10,7 +10,7 @@ import { existsSync, readFileSync } from 'node:fs'
 import { delimiter as pathDelimiter } from 'node:path'
 import type { Context } from 'cordis'
 import type { SessionEvent } from '@deepseek-ai/dsh-session'
-import type { GenerateOptions, LlmModelInfo, LlmProviderInfo, StreamChunk } from '@deepseek-ai/dsh-llm'
+import type { GenerateOptions, LlmModelContext, LlmModelInfo, LlmProviderInfo, StreamChunk } from '@deepseek-ai/dsh-llm'
 import { LlmAdapter, LlmError, assertNever } from '@deepseek-ai/dsh-llm'
 
 /**
@@ -31,6 +31,8 @@ export interface ReplayModelConfig {
   name?: string
   /** Optional selector description. */
   description?: string
+  /** Optional positive integer context capacity published by the replay adapter. */
+  contextWindow?: number
 }
 
 /** One provider route exposed by the replay adapter. */
@@ -260,6 +262,14 @@ class ReplayAdapter extends LlmAdapter {
     })))
   }
 
+  override resolveModelContext(provider: string, model: string): Promise<LlmModelContext | undefined> {
+    const configured = this.providers.get(provider)
+    /* v8 ignore next -- LlmService only asks about routes registered from this same map. */
+    if (configured === undefined) return Promise.resolve(undefined)
+    const contextWindow = configured.models?.find(candidate => candidate.id === model)?.contextWindow
+    return Promise.resolve(contextWindow === undefined ? undefined : { contextWindow })
+  }
+
   override stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
     return this.replay(options)
   }

+ 5 - 1
packages/support/llm-replay/tests/llm-replay.spec.ts

@@ -228,7 +228,7 @@ describe('installLlmReplay (through the real LlmService)', () => {
           id: 'deepseek',
           name: 'DeepSeek',
           models: [
-            { id: 'flash' },
+            { id: 'flash', contextWindow: 128_000 },
             { id: 'pro', name: 'Pro', description: 'Larger model' },
           ],
         },
@@ -245,6 +245,10 @@ describe('installLlmReplay (through the real LlmService)', () => {
       { provider: 'deepseek', id: 'pro', name: 'Pro', description: 'Larger model' },
     ])
     await expect(ctx.llm.listModels('empty')).resolves.toEqual([])
+    await expect(ctx.llm.resolveModelContext('deepseek', 'flash')).resolves.toEqual({ contextWindow: 128_000 })
+    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(await drain(ctx.llm.stream({ provider: 'deepseek', model: 'pro', messages: [] }))).toEqual(TEXT_CHUNKS)
 
     dispose()