Some checks failed
CI / windows node 24 / wine blocking (push) Has been skipped
CI / node 22.19 (push) Has been skipped
CI / node 26 (push) Has been skipped
CI / python 3.10 / keyless SDK (push) Has been skipped
CI / python runtime / release-shaped Linux x64 (push) Has been skipped
CI / wine apt cache (push) Successful in 7s
CI / serial / linux (push) Has been skipped
Deploy documentation / build (push) Failing after 1m25s
Deploy documentation / deploy (push) Has been skipped
Landlock Run / Matrix (push) Successful in 5s
Release (vendor) / Pack npm tarballs (push) Failing after 2m47s
Release (dsh) / Pack npm tarballs (push) Failing after 1m56s
Sandbox / sandbox e2e (landlock, ubuntu-24.04) (push) Failing after 1m57s
Sandbox / sandbox e2e (bwrap, ubuntu-latest) (push) Failing after 1m19s
Release (vendor) / Publish to npm (push) Has been skipped
Release (dsh) / Publish to npm (push) Has been skipped
CI / serial / windows (self-hosted standby) (push) Has been cancelled
CI / larger-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, typecheck) (push) Has been cancelled
Landlock Run / darwin (no platform package — degradation proof) (push) Has been cancelled
Landlock Run / ${{ matrix.platform }} (push) Has been cancelled
CI / node 24 / static (push) Has been cancelled
CI / node 24 / coverage (push) Has been cancelled
CI / node 24 / snapshots and artifacts (push) Has been cancelled
CI / windows node 24 / native complete (push) Has been cancelled
CI / serial / linux (self-hosted standby) (push) Has been cancelled
CI / serial / macos (push) Has been cancelled
CI / larger-runner-benchmark (16, windows, dsh-windows-2025-16core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (32, windows, dsh-windows-2025-32core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (4, windows, dsh-windows-2025-4core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (64, windows, dsh-windows-2025-64core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (8, windows, dsh-windows-2025-8core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (96, windows, dsh-windows-2025-96core, production-site) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, 16) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, windows, dsh-windows-2025-16core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, windows, dsh-windows-2025-32core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, 4) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, windows, dsh-windows-2025-4core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, windows, dsh-windows-2025-64core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, 8) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, windows, dsh-windows-2025-8core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, windows, dsh-windows-2025-96core, 2) (push) Has been cancelled
CI / all checks passed (push) Has been cancelled
Sandbox / sandbox e2e (seatbelt, macos-latest) (push) Has been cancelled
CI / larger-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, typecheck) (push) Has been cancelled
Sandbox / sandbox e2e (landlock, ubuntu-24.04-arm) (push) Has been cancelled
E2E (real DeepSeek API) / e2e (push) Failing after 1m24s
- new economy and maximum agent presets with three-role pipeline skill - new packages/extensions/tool-lab (home-lab ComfyUI/Docling/Whishper tools) - new packages/subagent/subagent-cursor provider - openrouter balance UI with on-demand refresh - session projection context-seed boundary fold - regenerate docs catalogs; keep local searxng benchmark scripts
208 lines
7.9 KiB
TypeScript
208 lines
7.9 KiB
TypeScript
/**
|
|
* REAL-composition proof: the shipped gateway YAML shape (session +
|
|
* projection registry + credentials + openrouter-usage) boots through the
|
|
* vendored Loader, the service default-export survives, a key resolved from
|
|
* the credentials document lets a mocked OpenRouter fetch populate the
|
|
* pricing table and the balance, and a logged step serves a priced
|
|
* `openRouterCost` view through the composed registry.
|
|
*/
|
|
|
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { pathToFileURL } from 'node:url'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { Context } from '@deepseek-ai/cordis'
|
|
import Loader from '@deepseek-ai/cordis-plugin-loader'
|
|
import Include from '@deepseek-ai/cordis-plugin-include'
|
|
import { createMessage } from '@deepseek-ai/dsh-llm'
|
|
import SessionStore from '@deepseek-ai/dsh-session'
|
|
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
|
|
import LocalCredentialProvider from '@deepseek-ai/dsh-credentials-local'
|
|
import FileSettingsProvider from '@deepseek-ai/dsh-settings-file'
|
|
import OpenRouterUsageGateway from '@deepseek-ai/dsh-openrouter-usage'
|
|
|
|
let root: string | undefined
|
|
let context: Context | undefined
|
|
|
|
afterEach(async () => {
|
|
await context?.fiber.dispose()
|
|
context = undefined
|
|
if (root !== undefined) await rm(root, { recursive: true, force: true })
|
|
root = undefined
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
/** Mock OpenRouter's read endpoints and record the calls. */
|
|
function stubOpenRouter() {
|
|
const pricing = [
|
|
{ id: 'deepseek/deepseek-chat', pricing: { prompt: '0.0000014', completion: '0.0000028', request: '0' } },
|
|
]
|
|
const credits = { total_credits: 42, total_usage: 1, is_free_tier: false }
|
|
const calls: string[] = []
|
|
vi.stubGlobal('fetch', vi.fn(async (input: RequestInfo | URL) => {
|
|
const url = String(input)
|
|
calls.push(url)
|
|
if (url.endsWith('/models')) {
|
|
return new Response(JSON.stringify({ data: pricing }), { status: 200 })
|
|
}
|
|
if (url.endsWith('/credits')) {
|
|
return new Response(JSON.stringify({ data: { ...credits } }), { status: 200 })
|
|
}
|
|
if (url.endsWith('/auth/key')) {
|
|
return new Response(JSON.stringify({
|
|
data: { label: 'test', usage: 1, limit: 1000, is_free_tier: false },
|
|
}), { status: 200 })
|
|
}
|
|
return new Response('not found', { status: 404 })
|
|
}))
|
|
return { calls, credits }
|
|
}
|
|
|
|
async function loadComposition(): Promise<Context> {
|
|
let rootDir = root
|
|
if (rootDir === undefined) {
|
|
rootDir = await mkdtemp(join(tmpdir(), 'dsh-openrouter-composition-'))
|
|
root = rootDir
|
|
}
|
|
await writeFile(join(rootDir, '.credentials.yaml'), 'OPENROUTER_API_KEY: sk-openrouter-test\n', { mode: 0o600 })
|
|
const settingsPath = join(rootDir, 'settings.yaml')
|
|
await writeFile(settingsPath, '# test settings\n')
|
|
await writeFile(join(rootDir, 'cordis.yml'), [
|
|
"- name: '@deepseek-ai/dsh-session'",
|
|
"- name: '@deepseek-ai/dsh-session-projection'",
|
|
'- id: settings',
|
|
" name: '@deepseek-ai/dsh-settings-file'",
|
|
' config:',
|
|
` path: ${JSON.stringify(settingsPath)}`,
|
|
' debounceMs: 10',
|
|
'- id: credentials',
|
|
" name: '@deepseek-ai/dsh-credentials-local'",
|
|
' config:',
|
|
` path: ${JSON.stringify(join(rootDir, '.credentials.yaml'))}`,
|
|
' debounceMs: 10',
|
|
"- name: '@deepseek-ai/dsh-openrouter-usage'",
|
|
' config:',
|
|
' syncEnabled: false',
|
|
'',
|
|
].join('\n'))
|
|
|
|
const ctx = new Context()
|
|
context = ctx
|
|
ctx.baseUrl = pathToFileURL(rootDir).href + '/'
|
|
await ctx.plugin(Loader)
|
|
ctx.loader.builtins.include = Include
|
|
const modules = new Map<string, unknown>([
|
|
['@deepseek-ai/dsh-session', SessionStore],
|
|
['@deepseek-ai/dsh-session-projection', SessionProjectionRegistry],
|
|
['@deepseek-ai/dsh-settings-file', FileSettingsProvider],
|
|
['@deepseek-ai/dsh-credentials-local', LocalCredentialProvider],
|
|
['@deepseek-ai/dsh-openrouter-usage', OpenRouterUsageGateway],
|
|
])
|
|
ctx.loader.internal = {
|
|
version: 'v2',
|
|
async import(specifier: string) {
|
|
if (!modules.has(specifier)) throw new Error(`unexpected Loader import: ${specifier}`)
|
|
return modules.get(specifier)
|
|
},
|
|
} as unknown as NonNullable<typeof ctx.loader.internal>
|
|
await ctx.loader.create({
|
|
name: 'cordis:include',
|
|
config: { path: pathToFileURL(join(rootDir, 'cordis.yml')).href },
|
|
})
|
|
await ctx.loader.await()
|
|
return ctx
|
|
}
|
|
|
|
describe('openrouter-usage real composition', () => {
|
|
it('resolves the key, fetches pricing, and prices a logged step through the composed registry', async () => {
|
|
const openRouter = stubOpenRouter()
|
|
const loaded = await loadComposition()
|
|
|
|
// Give the gateway's refresh a moment to run against the mock.
|
|
await vi.waitFor(() => {
|
|
expect(openRouter.calls.some(url => url.endsWith('/models'))).toBe(true)
|
|
}, { timeout: 5000 })
|
|
|
|
const session = loaded.sessions.create()
|
|
session.append('request/context', { provider: 'openrouter', model: 'deepseek/deepseek-chat' })
|
|
session.append('turn/start', { turn: 1 })
|
|
session.append('step/start', { turn: 1, step: 1 })
|
|
session.append('assistant/chunk', {
|
|
turn: 1,
|
|
step: 1,
|
|
chunk: { type: 'usage', usage: { inputTokens: 1_000, outputTokens: 200 } },
|
|
})
|
|
session.append('assistant/message', {
|
|
turn: 1,
|
|
step: 1,
|
|
message: createMessage({
|
|
role: 'assistant',
|
|
content: [{ type: 'text', text: 'hi' }],
|
|
source: { kind: 'model', provider: 'openrouter', model: 'deepseek/deepseek-chat' },
|
|
}),
|
|
usage: { inputTokens: 1_000, outputTokens: 200 },
|
|
}, { surfaceOp: 'append', sourceEventSeqs: [session.events.length - 1] })
|
|
session.append('step/end', { turn: 1, step: 1 })
|
|
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
|
|
|
const cost = loaded.sessionProjections.snapshot(session).values.openRouterCost
|
|
expect(cost).toMatchObject({
|
|
totalUsd: 1000 * 1.4e-6 + 200 * 2.8e-6,
|
|
pricedSteps: 1,
|
|
unknownModelSteps: 0,
|
|
currency: 'USD',
|
|
})
|
|
})
|
|
|
|
it('serves the account balance snapshot through the Remote gateway', async () => {
|
|
const openRouter = stubOpenRouter()
|
|
const loaded = await loadComposition()
|
|
|
|
await vi.waitFor(() => {
|
|
expect(openRouter.calls.some(url => url.endsWith('/credits'))).toBe(true)
|
|
}, { timeout: 5000 })
|
|
|
|
const balance = loaded.openRouterUsage.snapshot()
|
|
expect(balance.balanceUsd).toBe(41)
|
|
expect(balance.label).toBe('test')
|
|
expect(balance.currency).toBe('USD')
|
|
expect(balance.updatedAt).not.toBeNull()
|
|
})
|
|
|
|
it('re-fetches the account on demand and serves the moved figure', async () => {
|
|
const openRouter = stubOpenRouter()
|
|
const loaded = await loadComposition()
|
|
|
|
await vi.waitFor(() => {
|
|
expect(openRouter.calls.some(url => url.endsWith('/credits'))).toBe(true)
|
|
}, { timeout: 5000 })
|
|
expect(loaded.openRouterUsage.snapshot().balanceUsd).toBe(41)
|
|
|
|
openRouter.credits.total_usage = 12
|
|
await expect(loaded.openRouterUsage.refresh()).resolves.toMatchObject({ balanceUsd: 30 })
|
|
expect(loaded.openRouterUsage.snapshot().balanceUsd).toBe(30)
|
|
})
|
|
|
|
it('shares one in-flight fetch across concurrent on-demand refreshes', async () => {
|
|
const openRouter = stubOpenRouter()
|
|
const loaded = await loadComposition()
|
|
|
|
await vi.waitFor(() => {
|
|
expect(openRouter.calls.some(url => url.endsWith('/credits'))).toBe(true)
|
|
}, { timeout: 5000 })
|
|
const before = openRouter.calls.filter(url => url.endsWith('/credits')).length
|
|
|
|
const [first, second] = await Promise.all([
|
|
loaded.openRouterUsage.refresh(),
|
|
loaded.openRouterUsage.refresh(),
|
|
])
|
|
expect(first).toEqual(second)
|
|
expect(openRouter.calls.filter(url => url.endsWith('/credits')).length).toBe(before + 1)
|
|
|
|
// The shared promise is released once it settles, so a later click fetches again.
|
|
await loaded.openRouterUsage.refresh()
|
|
expect(openRouter.calls.filter(url => url.endsWith('/credits')).length).toBe(before + 2)
|
|
})
|
|
})
|