fix(llm-pi-ai): classify usage-based context overflow

Pass each resolved catalog model capacity into pi-ai stream conversion so the upstream full-message classifier can recognize provider-specific, silent, and length-stop overflow signals. Retain the harness text fallback for legacy provider wording and cover the catalog-resolution path with a mock-provider regression.
This commit is contained in:
Tianyi Cui
2026-07-19 14:18:13 +08:00
parent 44d9fbcd6d
commit 4791b40b26
5 changed files with 81 additions and 11 deletions
+25 -1
View File
@@ -2,9 +2,10 @@ import { createServer } from 'node:http'
import type { IncomingMessage, Server, ServerResponse } from 'node:http'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import LlmService, { LlmError, userAgent } from '@deepseek-ai/dsh-llm'
import LlmService, { CONTEXT_WINDOW_EXCEEDED_CODE, LlmError, userAgent } from '@deepseek-ai/dsh-llm'
import * as LlmPiAi from '@deepseek-ai/dsh-llm-pi-ai'
import { PiAiAdapter } from '@deepseek-ai/dsh-llm-pi-ai'
import { getModels } from '@earendil-works/pi-ai'
import { resolveProfiles } from '../src/config.ts'
import { assemble } from './assemble.ts'
@@ -178,6 +179,29 @@ describe('PiAiAdapter provider routing', () => {
const result = await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] })
expect(result.finish).toMatchObject({ kind: 'error', code })
})
it('uses the resolved catalog context window for usage-based overflow detection', async () => {
const model = getModels('deepseek').find(candidate => candidate.id === 'deepseek-v4-flash')
if (model === undefined) throw new Error('deepseek-v4-flash missing from pi-ai test catalog')
const events = [
'{"choices":[{"delta":{"role":"assistant","content":""},"index":0,"finish_reason":null}]}',
JSON.stringify({
choices: [{ delta: {}, index: 0, finish_reason: 'stop' }],
usage: { prompt_tokens: model.contextWindow + 1, completion_tokens: 0 },
}),
'[DONE]',
]
const server = await mockServer([{ events }])
const ctx = await harness(server.url)
const result = await assemble(ctx, { model: model.id, messages: [] })
expect(result.finish).toEqual({
kind: 'error',
message: `pi-ai detected context overflow for model "${model.id}"`,
code: CONTEXT_WINDOW_EXCEEDED_CODE,
})
})
})
describe('provider profile lifecycle', () => {
@@ -537,6 +537,34 @@ describe('mapStopReason / mapUsage', () => {
}))).toMatchObject({ kind: 'error', code: 'INVALID_REQUEST' })
})
it('uses pi-ai provider-specific overflow classification without losing rate-limit exclusions', () => {
expect(mapStopReason(assistant({
stopReason: 'error',
errorMessage: 'prompt is too long: 213462 tokens > 200000 maximum',
}))).toMatchObject({ kind: 'error', code: CONTEXT_WINDOW_EXCEEDED_CODE })
expect(mapStopReason(assistant({
stopReason: 'error',
errorMessage: 'ThrottlingException: Too many tokens, rate limit reached',
}))).toMatchObject({ kind: 'error', code: 'RATE_LIMIT' })
})
it('uses the resolved context window for silent and length-stop overflows', () => {
const silent = assistant({ stopReason: 'stop', usage: usage(101, 0) })
expect(mapStopReason(silent)).toEqual({ kind: 'stop' })
expect(mapStopReason(silent, 100)).toEqual({
kind: 'error',
message: 'pi-ai detected context overflow for model "deepseek-v4-flash"',
code: CONTEXT_WINDOW_EXCEEDED_CODE,
})
const truncated = assistant({ stopReason: 'length', usage: usage(80, 0, 19) })
expect(mapStopReason(truncated)).toEqual({ kind: 'max-tokens' })
expect(mapStopReason(truncated, 100)).toMatchObject({
kind: 'error',
code: CONTEXT_WINDOW_EXCEEDED_CODE,
})
})
it('maps cache fields only when nonzero', () => {
expect(mapUsage(usage(10, 5, 8, 2))).toEqual({
inputTokens: 10,