mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-14 04:01:35 +00:00
Merge remote-tracking branch 'origin/master' into worktree/plugin-list-display-cbe885
This commit is contained in:
@@ -252,6 +252,13 @@ jobs:
|
||||
name: node 22.19
|
||||
runner: ubuntu-latest
|
||||
gate_concurrency: '1'
|
||||
# Pinned inside 24.0-24.11.1: those releases carry the v1 internal
|
||||
# loader while reporting major 24, and every other job tracks the
|
||||
# latest 24, which is v2. A bare `24` here would retest that same v2.
|
||||
- node: '24.9'
|
||||
name: node 24.9
|
||||
runner: ubuntu-latest
|
||||
gate_concurrency: '1'
|
||||
- node: 26
|
||||
name: node 26
|
||||
runner: ubuntu-latest
|
||||
@@ -276,6 +283,12 @@ jobs:
|
||||
DSH_BUILD_CLIENT_PROFILE: official
|
||||
run: pnpm run check:node-compat
|
||||
|
||||
# Kept out of the gate aggregate: the shape a Node release carries only
|
||||
# changes with the Node version, so this belongs to the version matrix
|
||||
# rather than to every commit's checks.
|
||||
- name: Check Loader internal shape detection
|
||||
run: pnpm exec vitest run packages/boot/app-boot/tests/loader-shape.compat.spec.ts
|
||||
|
||||
python-sdk:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import Loader from '@deepseek-ai/cordis-plugin-loader'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
describe('Loader internal shape detection', () => {
|
||||
it('tags the running Node loader with the resolver signature that runtime accepts', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-loader-shape-'))
|
||||
const baseUrl = pathToFileURL(dir).href + '/'
|
||||
const ctx = new Context()
|
||||
ctx.baseUrl = baseUrl
|
||||
await ctx.plugin(Loader)
|
||||
try {
|
||||
const internal = ctx.loader.internal
|
||||
expect(internal, 'Node module internals are unreachable; HMR reload and client-module resolution both need them').toBeDefined()
|
||||
// Resolving through the tag is exactly what Hmr._resolve() and the
|
||||
// client-modules registry do. A tag taken from the Node major instead of
|
||||
// the loader's own API rejects every call on 24.0-24.11.1, which report
|
||||
// major 24 while carrying the v1 loader: v2 arrived only in 24.12.0.
|
||||
const resolved = internal!.version === 'v2'
|
||||
? internal!.resolveSync(baseUrl, { specifier: 'node:path', attributes: {} })
|
||||
: internal!.resolveSync('node:path', baseUrl, {})
|
||||
expect(resolved.url).toBe('node:path')
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -28,14 +28,13 @@ import type { Config } from '@deepseek-ai/dsh-llm-deepseek'
|
||||
import { assemble, type AssembledResult } from './assemble.ts'
|
||||
|
||||
/**
|
||||
* Real-API e2e for the direct-fetch adapter: V4 Flash + V4 Pro across
|
||||
* thinking modes and all official effort levels. The suite skips entirely
|
||||
* without $DEEPSEEK_API_KEY; the pre-release vision smoke additionally
|
||||
* Real-API e2e for the direct-fetch adapter: V4 Flash across thinking modes
|
||||
* and a max-effort tool round trip with reasoning passback. The suite skips
|
||||
* entirely without $DEEPSEEK_API_KEY; the pre-release vision smoke additionally
|
||||
* requires $DEEPSEEK_VISION_E2E=1 (see vitest.e2e.config.ts).
|
||||
*/
|
||||
|
||||
const FLASH = 'deepseek-v4-flash'
|
||||
const PRO = 'deepseek-v4-pro'
|
||||
const VISION = 'deepseek-v4-flash-vision-exp'
|
||||
const VISION_E2E_ENABLED = process.env.DEEPSEEK_VISION_E2E === '1'
|
||||
const TEST_PNG = Uint8Array.from(readFileSync(
|
||||
@@ -266,20 +265,23 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-deepseek e2e (real API)', ()
|
||||
expect(withThinking.usage?.reasoningTokens).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it.each(['high', 'max'] as const)(
|
||||
'pro + thinking enabled (effort %s): tool-call round trip with reasoning passback',
|
||||
async (effort) => {
|
||||
const ctx = await harness(PRO, { thinking: 'enabled' })
|
||||
it(
|
||||
'flash + thinking enabled (effort max): tool-call round trip with reasoning passback',
|
||||
async () => {
|
||||
const ctx = await harness(FLASH, { thinking: 'enabled' })
|
||||
|
||||
// Turn 1: the model must call the tool (and think before it).
|
||||
const first = await assemble(ctx,{
|
||||
model: PRO,
|
||||
reasoningEffort: ReasoningEffortId(effort),
|
||||
model: FLASH,
|
||||
reasoningEffort: ReasoningEffortId('max'),
|
||||
messages: ask('What is the weather in Paris right now? Use the get_weather tool.'),
|
||||
tools: [weatherTool],
|
||||
maxTokens: 2000,
|
||||
})
|
||||
expect(first.finish.kind).toBe('tool-calls')
|
||||
expect(
|
||||
first.finish.kind,
|
||||
`DeepSeek Flash tool-call turn finished as ${JSON.stringify(first.finish)}`,
|
||||
).toBe('tool-calls')
|
||||
const call = first.message.content.find(block => block.type === 'tool-call')
|
||||
expect(call).toBeDefined()
|
||||
expect(call!.name).toBe('get_weather')
|
||||
@@ -288,8 +290,8 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-deepseek e2e (real API)', ()
|
||||
// Turn 2: send the tool result back WITH the assistant's reasoning
|
||||
// block in history (the official thinking+tools passback rule).
|
||||
const second = await assemble(ctx,{
|
||||
model: PRO,
|
||||
reasoningEffort: ReasoningEffortId(effort),
|
||||
model: FLASH,
|
||||
reasoningEffort: ReasoningEffortId('max'),
|
||||
messages: [
|
||||
...ask('What is the weather in Paris right now? Use the get_weather tool.'),
|
||||
createMessage({
|
||||
@@ -308,22 +310,14 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-deepseek e2e (real API)', ()
|
||||
tools: [weatherTool],
|
||||
maxTokens: 2000,
|
||||
})
|
||||
expect(second.finish.kind).toBe('stop')
|
||||
expect(
|
||||
second.finish.kind,
|
||||
`DeepSeek Flash tool-result turn finished as ${JSON.stringify(second.finish)}`,
|
||||
).toBe('stop')
|
||||
expect(textOf(second).toLowerCase()).toMatch(/sunny|22/)
|
||||
},
|
||||
)
|
||||
|
||||
it('pro + thinking disabled: plain generation without reasoning blocks', async () => {
|
||||
const ctx = await harness(PRO, { thinking: 'disabled' })
|
||||
const result = await assemble(ctx,{
|
||||
model: PRO,
|
||||
messages: ask('Reply with exactly the word: pong'),
|
||||
maxTokens: 50,
|
||||
})
|
||||
expect(result.finish.kind).toBe('stop')
|
||||
expect(result.message.content.some(block => block.type === 'reasoning')).toBe(false)
|
||||
})
|
||||
|
||||
it('streams raw chunks in protocol order', async () => {
|
||||
const ctx = await harness(FLASH, { thinking: 'disabled' })
|
||||
const kinds: string[] = []
|
||||
|
||||
@@ -338,7 +338,7 @@ describe('DeepSeekAdapter against a mock server', () => {
|
||||
const ctx = await harness(server.url)
|
||||
|
||||
const result = await assemble(ctx, {
|
||||
model: 'deepseek-v4-flash',
|
||||
model: 'deepseek-v4-pro',
|
||||
messages: [createUserMessage({
|
||||
content: [{ type: 'text', text: 'hi' }],
|
||||
source: { kind: 'plugin', plugin: 'test' },
|
||||
@@ -350,7 +350,7 @@ describe('DeepSeekAdapter against a mock server', () => {
|
||||
|
||||
// The wire request carried the auth header contents we configured.
|
||||
expect(server.requests[0]).toMatchObject({
|
||||
model: 'deepseek-v4-flash',
|
||||
model: 'deepseek-v4-pro',
|
||||
max_tokens: 256_000,
|
||||
reasoning_effort: 'high',
|
||||
stream: true,
|
||||
|
||||
@@ -8,14 +8,12 @@ import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
|
||||
import { assemble, type AssembledResult } from './assemble.ts'
|
||||
|
||||
/**
|
||||
* Real-API e2e for the pi-ai-backed adapter: V4 Flash + V4 Pro with provider
|
||||
* defaults and representative off/high/max reasoning. Mirrors the native
|
||||
* adapter's StreamChunk contract and exercises a replayed tool follow-up.
|
||||
* Key-gated.
|
||||
* Real-API e2e for the pi-ai-backed adapter: V4 Flash defaults and
|
||||
* off/high/max reasoning. Mirrors the native adapter's StreamChunk contract
|
||||
* and exercises a replayed tool follow-up. Key-gated.
|
||||
*/
|
||||
|
||||
const FLASH = 'deepseek-v4-flash'
|
||||
const PRO = 'deepseek-v4-pro'
|
||||
const contexts: Context[] = []
|
||||
|
||||
async function harness(_model: string, config: Partial<PiAiProviderProfile> = {}) {
|
||||
@@ -67,10 +65,10 @@ const weatherTool: ToolSchema = {
|
||||
}
|
||||
|
||||
describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-pi-ai e2e (real API)', () => {
|
||||
it.each([FLASH, PRO])('%s + provider-default reasoning: plain text generation', async (model) => {
|
||||
const ctx = await harness(model)
|
||||
it(`${FLASH} + provider-default reasoning: plain text generation`, async () => {
|
||||
const ctx = await harness(FLASH)
|
||||
const result = await assemble(ctx,{
|
||||
model,
|
||||
model: FLASH,
|
||||
messages: ask('Reply with exactly the word: pong'),
|
||||
maxTokens: 50,
|
||||
})
|
||||
@@ -91,10 +89,10 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-pi-ai e2e (real API)', () =>
|
||||
expect(textOf(result).toLowerCase()).toContain('pong')
|
||||
})
|
||||
|
||||
it.each([FLASH, PRO])('%s + reasoning high: reasoning blocks present', async (model) => {
|
||||
const ctx = await harness(model)
|
||||
it(`${FLASH} + reasoning high: reasoning blocks present`, async () => {
|
||||
const ctx = await harness(FLASH)
|
||||
const result = await assemble(ctx,{
|
||||
model,
|
||||
model: FLASH,
|
||||
reasoningEffort: ReasoningEffortId('high'),
|
||||
messages: ask('Which is larger, 9.11 or 9.8? Answer with just the number.'),
|
||||
maxTokens: 2000,
|
||||
@@ -104,24 +102,27 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-pi-ai e2e (real API)', () =>
|
||||
expect(textOf(result)).toContain('9.8')
|
||||
})
|
||||
|
||||
it('pro + reasoning max: tool-call round trip', async () => {
|
||||
const ctx = await harness(PRO)
|
||||
it('flash + reasoning max: tool-call round trip', async () => {
|
||||
const ctx = await harness(FLASH)
|
||||
|
||||
const first = await assemble(ctx,{
|
||||
model: PRO,
|
||||
model: FLASH,
|
||||
reasoningEffort: ReasoningEffortId('max'),
|
||||
messages: ask('What is the weather in Paris right now? Use the get_weather tool.'),
|
||||
tools: [weatherTool],
|
||||
maxTokens: 2000,
|
||||
})
|
||||
expect(first.finish.kind).toBe('tool-calls')
|
||||
expect(
|
||||
first.finish.kind,
|
||||
`pi-ai Flash tool-call turn finished as ${JSON.stringify(first.finish)}`,
|
||||
).toBe('tool-calls')
|
||||
const call = first.message.content.find(block => block.type === 'tool-call')
|
||||
expect(call).toBeDefined()
|
||||
expect(call!.name).toBe('get_weather')
|
||||
expect(JSON.parse(call!.arguments)).toMatchObject({ city: expect.stringMatching(/paris/i) as string })
|
||||
|
||||
const second = await assemble(ctx,{
|
||||
model: PRO,
|
||||
model: FLASH,
|
||||
reasoningEffort: ReasoningEffortId('max'),
|
||||
messages: [
|
||||
...ask('What is the weather in Paris right now? Use the get_weather tool.'),
|
||||
@@ -138,7 +139,10 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-pi-ai e2e (real API)', () =>
|
||||
tools: [weatherTool],
|
||||
maxTokens: 2000,
|
||||
})
|
||||
expect(second.finish.kind).toBe('stop')
|
||||
expect(
|
||||
second.finish.kind,
|
||||
`pi-ai Flash tool-result turn finished as ${JSON.stringify(second.finish)}`,
|
||||
).toBe('stop')
|
||||
expect(textOf(second).toLowerCase()).toMatch(/sunny|22/)
|
||||
})
|
||||
|
||||
|
||||
@@ -135,14 +135,14 @@ describe('PiAiAdapter provider routing', () => {
|
||||
thinkingBudgets: { high: 2048 },
|
||||
})
|
||||
await assemble(ctx, {
|
||||
model: 'deepseek-v4-flash',
|
||||
model: 'deepseek-v4-pro',
|
||||
messages: [],
|
||||
temperature: 0.2,
|
||||
maxTokens: 77,
|
||||
sessionId: 'session-for-pi' as never,
|
||||
})
|
||||
expect(server.requests[0]).toMatchObject({
|
||||
model: 'deepseek-v4-flash',
|
||||
model: 'deepseek-v4-pro',
|
||||
temperature: 0.2,
|
||||
max_tokens: 77,
|
||||
thinking: { type: 'enabled' },
|
||||
|
||||
@@ -21,6 +21,7 @@ import * as claudeCode from '../src/index.ts'
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
const OFFICIAL_DEEPSEEK_BASE_URL = 'https://api.deepseek.com'
|
||||
const DEEPSEEK_MODEL = 'deepseek-v4-flash'
|
||||
const sdkRoot = dirname(fileURLToPath(
|
||||
import.meta.resolve('@anthropic-ai/claude-agent-sdk'),
|
||||
))
|
||||
@@ -90,11 +91,11 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)(
|
||||
const env = {
|
||||
ANTHROPIC_AUTH_TOKEN: apiKey,
|
||||
ANTHROPIC_BASE_URL: `${deepSeekBaseUrl()}/anthropic`,
|
||||
ANTHROPIC_MODEL: 'deepseek-v4-pro[1m]',
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: 'deepseek-v4-pro[1m]',
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: 'deepseek-v4-pro[1m]',
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'deepseek-v4-flash',
|
||||
CLAUDE_CODE_SUBAGENT_MODEL: 'deepseek-v4-flash',
|
||||
ANTHROPIC_MODEL: DEEPSEEK_MODEL,
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: DEEPSEEK_MODEL,
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: DEEPSEEK_MODEL,
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: DEEPSEEK_MODEL,
|
||||
CLAUDE_CODE_SUBAGENT_MODEL: DEEPSEEK_MODEL,
|
||||
CLAUDE_CODE_EFFORT_LEVEL: 'max',
|
||||
CLAUDE_CONFIG_DIR: claudeConfig,
|
||||
HOME: root,
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
name: '@deepseek-ai/dsh-acp'
|
||||
config:
|
||||
provider: deepseek-official
|
||||
model: deepseek-v4-pro
|
||||
model: deepseek-v4-flash
|
||||
|
||||
- id: system-prompt
|
||||
name: '@deepseek-ai/dsh-system-prompt'
|
||||
|
||||
Vendored
+1
@@ -48,6 +48,7 @@ Keep this log exhaustive — every divergence from upstream must be listed.
|
||||
16. **`cordis/package.json` publishes `src`**: added `src` to the `files` list, joining the other eight vendored packages. Cordis declares `"./src/*": "./src/*"` in its exports, so a tarball without `src` publishes an export map pointing at absent files; the release change judgement also reads `files` to decide whether a diff reaches the payload, and a package whose only published paths are build output has no tracked path to match.
|
||||
17. **`@deepseek-ai` rescope**: every vendored manifest `name`, every internal dependency entry among the vendored set, and every module specifier that reaches them use the scoped names in the manifest table's `npm name` column. Directory names, version numbers, and dependency ranges are unchanged, and no upstream runtime identifier is renamed — `Symbol.for('schemastery')` and Schemastery's `vendor:` metadata field keep their upstream values. Re-apply with `pnpm run rescope-vendor --apply` after a sync; the table's two name columns are the mapping, restated for consumers in [docs/rescope.md](../docs/rescope.md).
|
||||
18. **Entry `disabled` interpolation in `loader/src/config/entry.ts`**: a `disabled: !!js` expression evaluates against the loader context at every mount decision; the raw node stays in the options, so write-back keeps the `!!js` form. `disabled` is the only interpolated metadata field. Covered by `packages/boot/app-boot/tests/user-patches.spec.ts` and `apps/cli/tests/windows-shell.spec.ts`.
|
||||
19. **`loader/src/internal.ts` runtime shape detection**: `ModuleLoader.fromInternal()` classifies the internal loader by which module-job API it owns — `getOrCreateModuleJob` for v2, `getModuleJobForImport` for v1 — instead of by Node major version. Upstream tags every major `>= 24` as v2, but the v2 interface arrived in Node 24.12.0, so 24.0–24.11.1 report major 24 while still carrying the v1 loader; consumers then called `resolveSync` with reversed parameters and every call threw. `dsh web` served an empty client graph (`__DSH_BOOT__.entries: []`) and HMR partial reload resolved no entry URL, both behind swallowed or warn-level errors. Arity cannot discriminate the two shapes, because each reports `resolveSync.length === 2`. A loader owning neither API is left unclassified rather than guessed, so consumers take their documented no-internals path. Covered on the `node-compat` Node version matrix, which pins 24.9 for the mistagged range.
|
||||
|
||||
## Sync procedure
|
||||
|
||||
|
||||
Vendored
+19
-7
@@ -117,16 +117,28 @@ export namespace ModuleLoader {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate and classify the running Node internal module loader.
|
||||
*
|
||||
* The shape is decided by which module-job API the loader owns, never by the
|
||||
* Node version: v2 landed in 24.12.0, so a major-version test mistags every
|
||||
* 24.0–24.11.1 loader as v2 and makes consumers call `resolveSync` with
|
||||
* reversed parameters. Arity is not usable either — `resolveSync` reports 2
|
||||
* under both shapes. A loader owning neither API is left unclassified rather
|
||||
* than guessed, so consumers take their documented no-internals path.
|
||||
* @returns the classified loader, or `undefined` when none is reachable or its shape is unknown.
|
||||
*/
|
||||
export function fromInternal(): ModuleLoader | undefined {
|
||||
if (_cachedLoader) return _cachedLoader
|
||||
const [major] = process.versions.node.split('.').map(Number)
|
||||
if (major < 22) return
|
||||
|
||||
if (major >= 24) {
|
||||
const raw = requireInternal('internal/modules/esm/loader')?.getOrInitializeCascadedLoader()
|
||||
if (raw) return _cachedLoader = Object.assign(raw, { version: 'v2' })
|
||||
} else if (major >= 22) {
|
||||
const raw = requireInternal('internal/modules/esm/loader')?.getOrInitializeCascadedLoader()
|
||||
if (raw) return _cachedLoader = Object.assign(raw, { version: 'v1' })
|
||||
}
|
||||
const raw = requireInternal('internal/modules/esm/loader')?.getOrInitializeCascadedLoader()
|
||||
if (!raw) return
|
||||
const version = typeof raw.getOrCreateModuleJob === 'function'
|
||||
? 'v2'
|
||||
: typeof raw.getModuleJobForImport === 'function' ? 'v1' : undefined
|
||||
if (!version) return
|
||||
return _cachedLoader = Object.assign(raw, { version })
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user