refactor: apply repository naming contract
Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/llm/llm-deepseek/README.md
|
||||
README.md: 1fde02dc8c764a189eb78226de4c194bbf0b8a5e
|
||||
README.zh.md: 711bc92101c4df0abd40375e3d73f993105ea81c
|
||||
README.md: 1923be694c20149fe5387bd7bb4dac5da32a63a4
|
||||
README.zh.md: bdce7f2b2fdff0e1542deb3e7fae5c5e7139d13b
|
||||
|
||||
@@ -39,7 +39,7 @@ The plugin registers the single provider route `deepseek-official` together with
|
||||
|
||||
`contextWindow` is optional per configured model and is not exposed through the advisory catalog. `ctx.llm.resolveModelInfo('deepseek-official', model).context` returns an exact model value first, then `defaultContextWindow` for an entry without capacity or an unlisted pass-through id. The adapter default is 1,000,000; pressure-sensitive plugins therefore get deployment-owned capacity without treating the model selector as authoritative. Registering another adapter for `deepseek-official` throws `LlmError('DUPLICATE_ADAPTER')`.
|
||||
|
||||
`maxTokens` is the adapter-configured output cap for conversation requests and defaults to 256,000. A catalog entry may carry its own `maxTokens`, which wins for that model; an entry without one, and any unlisted pass-through id, resolve to the profile value, so adding a per-model cap changes one model rather than the route. Exact-model resolution exposes the winner as `defaultMaxTokens`; `LlmService` materializes that value into `GenerateOptions.maxTokens` before the agent loop writes `request/header`, so the wire request remains reconstructable. An explicit request or `AgentOptions.maxTokens` value wins and is serialized as `max_tokens`. The adapter does not clamp this request budget against `contextWindow`; deployments with a smaller context or provider output limit must configure a compatible `maxTokens`.
|
||||
`maxTokens` is the adapter-configured output cap for conversation requests and defaults to 256,000. A catalog entry may carry its own `maxTokens`, which wins for that model; an entry without one, and any unlisted pass-through id, resolve to the profile value, so adding a per-model cap changes one model rather than the route. Exact-model resolution exposes the winner as `defaultMaxTokens`; `LlmRuntime` materializes that value into `GenerateOptions.maxTokens` before the agent loop writes `request/header`, so the wire request remains reconstructable. An explicit request or `AgentOptions.maxTokens` value wins and is serialized as `max_tokens`. The adapter does not clamp this request budget against `contextWindow`; deployments with a smaller context or provider output limit must configure a compatible `maxTokens`.
|
||||
|
||||
The same exact-model result exposes ordered `off`, `high`, and `max` efforts under `reasoning` for every pass-through model when deployment policy permits thinking. `reasoningEffort` selects the deployment default and falls back to `high` when omitted. `agent/request` can replace it on each conversation step; the resolved value is logged in `request/header`. `high` and `max` enable thinking and serialize as the official top-level `reasoning_effort`; adapter-owned `off` instead serializes `thinking.type: disabled` and omits `reasoning_effort`. An unsupported value fails with `UNSUPPORTED_REASONING_EFFORT` before network I/O.
|
||||
|
||||
@@ -60,9 +60,9 @@ The plugin also declares its route in the configurable-provider directory (`ctx.
|
||||
|
||||
## App attribution
|
||||
|
||||
Every request carries the shared attribution header from dsh-llm's `attributionHeaders()` - the mandatory `User-Agent` baseline identifying the harness (see [dsh-llm § App attribution](../llm/README.md#app-attribution-attributionts)). Direct DeepSeek requests and OpenAI-compatible gateway requests get no provider-specific app-attribution headers under this adapter contract; OpenRouter app attribution is deferred to a future explicit OpenRouter adapter or mode. A request whose `GenerateOptions.purpose` is `compaction` (dsh-compact-basic's auxiliary summarization call) additionally carries `x-deepseek-harness-compact: 1`, so the host can separate compaction traffic from conversation requests.
|
||||
Every request carries the shared attribution header from dsh-llm's `attributionHeaders()` - the mandatory `User-Agent` baseline identifying the harness (see [dsh-llm § App attribution](../llm/README.md#app-attribution-attributionts)). Direct DeepSeek requests and OpenAI-compatible gateway requests get no provider-specific app-attribution headers under this adapter contract; OpenRouter app attribution is deferred to a future explicit OpenRouter adapter or mode. A request whose `GenerateOptions.purpose` is `compaction` (dsh-compaction-basic's auxiliary summarization call) additionally carries `x-deepseek-harness-compact: 1`, so the host can separate compaction traffic from conversation requests.
|
||||
|
||||
DeepSeek request identity is separate from app attribution. After credential resolution, every provider request carries `x-deepseek-harness-user-id` with the stable anonymous id from [`@deepseek-ai/dsh-user-id`](../../session/user-id/README.md); a request carrying `GenerateOptions.sessionId` also sends that exact value as `x-deepseek-harness-session-id`, while a direct call without a session omits the session header. Both headers go to the resolved `baseURL`, including a configured gateway, and remain outside the request body and model-visible content.
|
||||
DeepSeek request identity is separate from app attribution. After credential resolution, every provider request carries `x-deepseek-harness-user-id` with the stable anonymous id from [`@deepseek-ai/dsh-anonymous-user-id`](../../identity/anonymous-user-id/README.md); a request carrying `GenerateOptions.sessionId` also sends that exact value as `x-deepseek-harness-session-id`, while a direct call without a session omits the session header. Both headers go to the resolved `baseURL`, including a configured gateway, and remain outside the request body and model-visible content.
|
||||
|
||||
## Wire-format notes
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ harness LLM(大语言模型)seam 的 DeepSeek chat-completions 适配器:
|
||||
|
||||
`contextWindow` 对每个已配置模型都可选,不会通过建议 catalog 公开。`ctx.llm.resolveModelInfo('deepseek-official', model).context` 先返回精确模型值,再对不含容量的配置项或未列出原样传递 id 返回 `defaultContextWindow`。适配器默认值为 1,000,000;因此,压力敏感插件可以获得由部署决定的容量,不会将模型 selector 视为权威。为 `deepseek-official` 注册另一个适配器会抛出 `LlmError('DUPLICATE_ADAPTER')`。
|
||||
|
||||
`maxTokens` 是适配器为对话请求配置的输出上限,默认值为 256,000。Catalog 配置项可以自带 `maxTokens`,它对该模型胜出;不含该上限的配置项以及任何未列出原样传递 id 都解析为 profile 值,因此新增按模型的上限只改变一个模型,而非整条路由。确切模型解析会将胜出值公开为 `defaultMaxTokens`;`LlmService` 会在 agent loop(智能体循环)写入 `request/header` 前,将该值填入 `GenerateOptions.maxTokens`,从而仍可根据持久记录重建协议请求。显式的请求值或 `AgentOptions.maxTokens` 值优先,并会序列化为 `max_tokens`。适配器不会根据 `contextWindow` 自动调低该请求预算;上下文或提供方输出上限较小的部署必须配置与其相容的 `maxTokens`。
|
||||
`maxTokens` 是适配器为对话请求配置的输出上限,默认值为 256,000。Catalog 配置项可以自带 `maxTokens`,它对该模型胜出;不含该上限的配置项以及任何未列出原样传递 id 都解析为 profile 值,因此新增按模型的上限只改变一个模型,而非整条路由。确切模型解析会将胜出值公开为 `defaultMaxTokens`;`LlmRuntime` 会在 agent loop(智能体循环)写入 `request/header` 前,将该值填入 `GenerateOptions.maxTokens`,从而仍可根据持久记录重建协议请求。显式的请求值或 `AgentOptions.maxTokens` 值优先,并会序列化为 `max_tokens`。适配器不会根据 `contextWindow` 自动调低该请求预算;上下文或提供方输出上限较小的部署必须配置与其相容的 `maxTokens`。
|
||||
|
||||
同一确切模型结果会在部署策略允许思考时,为每个原样传递模型在 `reasoning` 下公开有序的 `off`、`high` 和 `max` 推理(reasoning)强度。`reasoningEffort` 选择部署默认值,省略时回退为 `high`。`agent/request` 可以在每个会话步骤替换它;解析后的值会记录在 `request/header`。`high` 和 `max` 会启用思考,并序列化为官方顶层 `reasoning_effort`;适配器持有的 `off` 则序列化为 `thinking.type: disabled`,且省略 `reasoning_effort`。不支持的值会在网络 I/O 前以 `UNSUPPORTED_REASONING_EFFORT` 失败。
|
||||
|
||||
@@ -60,9 +60,9 @@ harness LLM(大语言模型)seam 的 DeepSeek chat-completions 适配器:
|
||||
|
||||
## 应用归因
|
||||
|
||||
每个请求都携带 dsh-llm `attributionHeaders()` 的共享归因标头,即用于识别 harness 的必需 `User-Agent` 基线(见 [dsh-llm § 应用归因](../llm/README.md#app-attribution-attributionts))。在该适配器约定(adapter contract)下,直接 DeepSeek 请求与 OpenAI 兼容 gateway 请求都不会获得提供方特定应用归因标头;OpenRouter 应用归因暂缓到未来的显式 OpenRouter 适配器或模式。`GenerateOptions.purpose` 为 `compaction` 的请求(dsh-compact-basic 的辅助摘要调用)还会携带 `x-deepseek-harness-compact: 1`,让宿主可以将压缩流量与会话请求分开。
|
||||
每个请求都携带 dsh-llm `attributionHeaders()` 的共享归因标头,即用于识别 harness 的必需 `User-Agent` 基线(见 [dsh-llm § 应用归因](../llm/README.md#app-attribution-attributionts))。在该适配器约定(adapter contract)下,直接 DeepSeek 请求与 OpenAI 兼容 gateway 请求都不会获得提供方特定应用归因标头;OpenRouter 应用归因暂缓到未来的显式 OpenRouter 适配器或模式。`GenerateOptions.purpose` 为 `compaction` 的请求(dsh-compaction-basic 的辅助摘要调用)还会携带 `x-deepseek-harness-compact: 1`,让宿主可以将压缩流量与会话请求分开。
|
||||
|
||||
DeepSeek 请求身份独立于应用归因。凭据解析成功后,每个提供方请求都会通过 `x-deepseek-harness-user-id` 携带来自 [`@deepseek-ai/dsh-user-id`](../../session/user-id/README.md) 的稳定匿名 id;携带 `GenerateOptions.sessionId` 的请求还会通过 `x-deepseek-harness-session-id` 发送该确切值,缺少会话的直接调用则省略会话标头。两个标头都会发送至解析后的 `baseURL`(包括已配置的 gateway),且不会进入请求正文或模型可见内容。
|
||||
DeepSeek 请求身份独立于应用归因。凭据解析成功后,每个提供方请求都会通过 `x-deepseek-harness-user-id` 携带来自 [`@deepseek-ai/dsh-anonymous-user-id`](../../identity/anonymous-user-id/README.md) 的稳定匿名 id;携带 `GenerateOptions.sessionId` 的请求还会通过 `x-deepseek-harness-session-id` 发送该确切值,缺少会话的直接调用则省略会话标头。两个标头都会发送至解析后的 `baseURL`(包括已配置的 gateway),且不会进入请求正文或模型可见内容。
|
||||
|
||||
## 协议格式说明
|
||||
|
||||
|
||||
@@ -33,12 +33,12 @@
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-credentials": "workspace:^",
|
||||
"@deepseek-ai/dsh-environment": "workspace:^",
|
||||
"@deepseek-ai/dsh-launch-environment": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-settings": "workspace:^",
|
||||
"@deepseek-ai/dsh-timeout": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-id": "workspace:^",
|
||||
"@deepseek-ai/dsh-anonymous-user-id": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -47,12 +47,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-credentials": "workspace:^",
|
||||
"@deepseek-ai/dsh-environment": "workspace:^",
|
||||
"@deepseek-ai/dsh-launch-environment": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-settings": "workspace:^",
|
||||
"@deepseek-ai/dsh-timeout": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-id": "workspace:^",
|
||||
"@deepseek-ai/dsh-anonymous-user-id": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import type {
|
||||
} from '@deepseek-ai/dsh-llm'
|
||||
import type { CredentialRef } from '@deepseek-ai/dsh-credentials'
|
||||
import { idleWatchdog, timeoutOf } from '@deepseek-ai/dsh-timeout'
|
||||
import type { AnonymousUserId } from '@deepseek-ai/dsh-user-id'
|
||||
import type { AnonymousUserId } from '@deepseek-ai/dsh-anonymous-user-id'
|
||||
import { serializeRequest } from './serialize.ts'
|
||||
import type { RequestDefaults } from './serialize.ts'
|
||||
import { parseSse } from './sse.ts'
|
||||
|
||||
@@ -16,10 +16,10 @@ import z from '@deepseek-ai/schemastery'
|
||||
import { assertUsableApiKey, LlmError, resolveRetryPolicy, RetryPolicySchema } from '@deepseek-ai/dsh-llm'
|
||||
import type { RetryPolicyConfig } from '@deepseek-ai/dsh-llm'
|
||||
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
||||
import { environmentOf, type EnvironmentSnapshot } from '@deepseek-ai/dsh-environment'
|
||||
import { launchEnvironmentOf, type LaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment'
|
||||
import { deepEqualJson, installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
|
||||
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
|
||||
import { getOrCreateAnonymousUserId, type AnonymousUserId } from '@deepseek-ai/dsh-user-id'
|
||||
import { getOrCreateAnonymousUserId, type AnonymousUserId } from '@deepseek-ai/dsh-anonymous-user-id'
|
||||
import {
|
||||
DEFAULT_CONTEXT_WINDOW,
|
||||
DEFAULT_MAX_TOKENS,
|
||||
@@ -158,7 +158,7 @@ function resolveModels(models: readonly DeepSeekCatalogModel[] | undefined): Dee
|
||||
* gateway that checkout is meant to use.
|
||||
* @returns validated connection facts plus the credential reference.
|
||||
*/
|
||||
export function resolveAdapterOptions(config: Config, environment?: EnvironmentSnapshot): ResolvedDeepSeekOptions {
|
||||
export function resolveAdapterOptions(config: Config, environment?: LaunchEnvironmentSnapshot): ResolvedDeepSeekOptions {
|
||||
if (config.thinking === 'disabled'
|
||||
&& config.reasoningEffort !== undefined
|
||||
&& config.reasoningEffort !== 'off') {
|
||||
@@ -205,7 +205,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
const raw = current()
|
||||
if (raw === lastRaw && lastGood !== undefined) return lastGood
|
||||
try {
|
||||
const next = resolveAdapterOptions(raw, environmentOf(ctx))
|
||||
const next = resolveAdapterOptions(raw, launchEnvironmentOf(ctx))
|
||||
lastRaw = raw
|
||||
lastGood = next
|
||||
return next
|
||||
@@ -233,7 +233,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
} else {
|
||||
// Without the seam there is no managed store to rank against, so the
|
||||
// environment is the whole credential plane.
|
||||
const ambient = environmentOf(ctx).get(ref)
|
||||
const ambient = launchEnvironmentOf(ctx).get(ref)
|
||||
if (ambient !== undefined && ambient.value.length > 0) {
|
||||
return assertUsableApiKey(ambient.value, 'llm-deepseek', ref)
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import LlmService, { createUserMessage, CallId, ReasoningEffortId , createMessage } from '@deepseek-ai/dsh-llm'
|
||||
import LlmRuntime, { createUserMessage, CallId, ReasoningEffortId , createMessage } from '@deepseek-ai/dsh-llm'
|
||||
import type { Message, ToolSchema } from '@deepseek-ai/dsh-llm'
|
||||
import { CredentialsLocal } from '@deepseek-ai/dsh-credentials-local'
|
||||
import { LocalCredentialProvider } from '@deepseek-ai/dsh-credentials-local'
|
||||
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
|
||||
import type { Config } from '@deepseek-ai/dsh-llm-deepseek'
|
||||
import { assemble, type AssembledResult } from './assemble.ts'
|
||||
@@ -29,7 +29,7 @@ beforeEach(async () => {
|
||||
async function harness(_model: string, config: Partial<Config> = {}) {
|
||||
const ctx = new Context()
|
||||
contexts.push(ctx)
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
await ctx.plugin(LlmDeepSeek, config)
|
||||
return ctx
|
||||
}
|
||||
@@ -78,8 +78,8 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-deepseek e2e (real API)', ()
|
||||
vi.stubEnv('DEEPSEEK_API_KEY', '')
|
||||
const ctx = new Context()
|
||||
contexts.push(ctx)
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(CredentialsLocal, { path: join(dir, '.credentials.yaml'), watch: false })
|
||||
await ctx.plugin(LlmRuntime)
|
||||
await ctx.plugin(LocalCredentialProvider, { path: join(dir, '.credentials.yaml'), watch: false })
|
||||
await ctx.plugin(LlmDeepSeek, {})
|
||||
|
||||
const result = await assemble(ctx, {
|
||||
|
||||
@@ -3,8 +3,8 @@ import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { createEnvironmentSnapshot } from '@deepseek-ai/dsh-environment'
|
||||
import LlmService, { createUserMessage,
|
||||
import { createLaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment'
|
||||
import LlmRuntime, { createUserMessage,
|
||||
CONTEXT_WINDOW_EXCEEDED_CODE,
|
||||
ProviderRequestId,
|
||||
QUOTA_EXCEEDED_CODE,
|
||||
@@ -12,7 +12,7 @@ import LlmService, { createUserMessage,
|
||||
userAgent,
|
||||
} from '@deepseek-ai/dsh-llm'
|
||||
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
|
||||
import { getOrCreateAnonymousUserId, type AnonymousUserId } from '@deepseek-ai/dsh-user-id'
|
||||
import { getOrCreateAnonymousUserId, type AnonymousUserId } from '@deepseek-ai/dsh-anonymous-user-id'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
|
||||
import { DeepSeekAdapter, resolveAdapterOptions } from '@deepseek-ai/dsh-llm-deepseek'
|
||||
@@ -41,7 +41,7 @@ async function harness(baseURL: string, config: object = {}) {
|
||||
// environment, which is the whole credential plane without a mounted seam.
|
||||
vi.stubEnv('DEEPSEEK_API_KEY', 'test-key')
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
await ctx.plugin(LlmDeepSeek, { baseURL, ...config })
|
||||
return ctx
|
||||
}
|
||||
@@ -617,7 +617,7 @@ describe('plugin registration and config', () => {
|
||||
it('registers the deepseek provider and unregisters on dispose (HMR safety)', async () => {
|
||||
const server = await mockServer([])
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
const fiber = await ctx.plugin(LlmDeepSeek, {
|
||||
baseURL: server.url,
|
||||
})
|
||||
@@ -635,7 +635,7 @@ describe('plugin registration and config', () => {
|
||||
|
||||
it('registers retryPolicy from the provider config', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
await ctx.plugin(LlmDeepSeek, {
|
||||
baseURL: 'http://127.0.0.1:1',
|
||||
retryPolicy: {
|
||||
@@ -654,7 +654,7 @@ describe('plugin registration and config', () => {
|
||||
|
||||
it('owns the deepseek provider and advertises the default models', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
await ctx.plugin(LlmDeepSeek, { baseURL: 'http://127.0.0.1:1' })
|
||||
expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek-official', name: 'DeepSeek' }])
|
||||
await expect(ctx.llm.listModels('deepseek-official')).resolves.toEqual([
|
||||
@@ -681,7 +681,7 @@ describe('plugin registration and config', () => {
|
||||
|
||||
it.each(['off', 'max'] as const)('uses the configured %s reasoning default', async (effort) => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
await ctx.plugin(LlmDeepSeek, {
|
||||
baseURL: 'http://127.0.0.1:1',
|
||||
reasoningEffort: effort,
|
||||
@@ -701,7 +701,7 @@ describe('plugin registration and config', () => {
|
||||
|
||||
it('accepts off as the default when thinking is deployment-disabled', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
await ctx.plugin(LlmDeepSeek, {
|
||||
baseURL: 'http://127.0.0.1:1',
|
||||
thinking: 'disabled',
|
||||
@@ -720,7 +720,7 @@ describe('plugin registration and config', () => {
|
||||
'rejects configured reasoning effort %s when thinking is disabled',
|
||||
async (reasoningEffort) => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
await expect(ctx.plugin(LlmDeepSeek, {
|
||||
baseURL: 'http://127.0.0.1:1',
|
||||
thinking: 'disabled',
|
||||
@@ -750,7 +750,7 @@ describe('plugin registration and config', () => {
|
||||
|
||||
it('uses the default model catalog when apply is called directly', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
LlmDeepSeek.apply(ctx, { baseURL: 'http://127.0.0.1:1' })
|
||||
await expect(ctx.llm.listModels('deepseek-official')).resolves.toEqual([
|
||||
{ provider: 'deepseek-official', id: 'deepseek-v4-flash', name: 'DeepSeek-V4-Flash', inputModalities: ['text'] },
|
||||
@@ -760,7 +760,7 @@ describe('plugin registration and config', () => {
|
||||
|
||||
it('advertises configured models without restricting arbitrary request ids', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
await ctx.plugin(LlmDeepSeek, {
|
||||
baseURL: 'http://127.0.0.1:1',
|
||||
models: [
|
||||
@@ -793,7 +793,7 @@ describe('plugin registration and config', () => {
|
||||
|
||||
it('uses exact model capacity before the adapter-wide default', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
await ctx.plugin(LlmDeepSeek, {
|
||||
baseURL: 'http://127.0.0.1:1',
|
||||
defaultContextWindow: 256_000,
|
||||
@@ -813,7 +813,7 @@ describe('plugin registration and config', () => {
|
||||
|
||||
it('allows an explicit empty model catalog', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
await ctx.plugin(LlmDeepSeek, {
|
||||
baseURL: 'http://127.0.0.1:1',
|
||||
models: [],
|
||||
@@ -829,7 +829,7 @@ describe('plugin registration and config', () => {
|
||||
[[{ id: 'm' }, { id: 'm' }], /duplicate catalog model/],
|
||||
] as const)('rejects invalid advisory model config', async (models, message) => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
await expect(ctx.plugin(LlmDeepSeek, {
|
||||
baseURL: 'http://127.0.0.1:1',
|
||||
models: [...models],
|
||||
@@ -859,7 +859,7 @@ describe('plugin registration and config', () => {
|
||||
|
||||
it('rejects invalid context capacity when apply is called directly', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
expect(() => {
|
||||
LlmDeepSeek.apply(ctx, {
|
||||
baseURL: 'http://127.0.0.1:1',
|
||||
@@ -876,7 +876,7 @@ describe('plugin registration and config', () => {
|
||||
.toThrow(/defaultContextWindow must be a positive integer/)
|
||||
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
await expect(ctx.plugin(LlmDeepSeek, {
|
||||
baseURL: 'http://127.0.0.1:1',
|
||||
defaultContextWindow,
|
||||
@@ -892,7 +892,7 @@ describe('plugin registration and config', () => {
|
||||
.toThrow(/maxTokens must be a positive safe integer/)
|
||||
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
await expect(ctx.plugin(LlmDeepSeek, {
|
||||
baseURL: 'http://127.0.0.1:1',
|
||||
maxTokens,
|
||||
@@ -905,7 +905,7 @@ describe('plugin registration and config', () => {
|
||||
vi.stubEnv('DEEPSEEK_API_KEY', 'env-key')
|
||||
vi.stubEnv('DEEPSEEK_BASE_URL', 'http://127.0.0.1:1')
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
await ctx.plugin(LlmDeepSeek, {})
|
||||
expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek-official', name: 'DeepSeek' }])
|
||||
})
|
||||
@@ -913,7 +913,7 @@ describe('plugin registration and config', () => {
|
||||
it('loads keyless, keeps the catalog browsable, and fails the request actionably', async () => {
|
||||
vi.stubEnv('DEEPSEEK_API_KEY', '')
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
await ctx.plugin(LlmDeepSeek, { baseURL: 'http://127.0.0.1:1' })
|
||||
// First-boot onboarding: the route registers so models stay discoverable;
|
||||
// only the request itself needs a key.
|
||||
@@ -937,7 +937,7 @@ describe('plugin registration and config', () => {
|
||||
vi.stubEnv('DEEPSEEK_API_KEY', 'ambient-key')
|
||||
const server = await mockServer([{ kind: 'sse', events: textEvents }])
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
await ctx.plugin(LlmDeepSeek, { baseURL: server.url })
|
||||
await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] })
|
||||
expect(server.headers[0]?.authorization).toBe('Bearer ambient-key')
|
||||
@@ -946,7 +946,7 @@ describe('plugin registration and config', () => {
|
||||
it('treats an empty ambient variable as no key when no credentials seam is mounted', async () => {
|
||||
vi.stubEnv('DEEPSEEK_API_KEY', '')
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
await ctx.plugin(LlmDeepSeek, { baseURL: 'http://127.0.0.1:1' })
|
||||
const result = await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] })
|
||||
expect(result.finish).toMatchObject({ kind: 'error', failure: { code: 'MISSING_CREDENTIAL' } })
|
||||
@@ -966,7 +966,7 @@ describe('plugin registration and config', () => {
|
||||
vi.stubEnv('DEEPSEEK_BASE_URL', server.url)
|
||||
vi.stubEnv('DEEPSEEK_API_KEY', 'test-key')
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
await ctx.plugin(LlmDeepSeek, {})
|
||||
await assemble(ctx,{ model: 'deepseek-v4-flash', messages: [] })
|
||||
expect(server.requests).toHaveLength(1)
|
||||
@@ -974,19 +974,19 @@ describe('plugin registration and config', () => {
|
||||
|
||||
|
||||
it('takes DEEPSEEK_BASE_URL from any environment layer, with explicit config still on top', () => {
|
||||
const trusted = createEnvironmentSnapshot([
|
||||
const trusted = createLaunchEnvironmentSnapshot([
|
||||
{ source: 'user-env', path: '/home/.dsh/.env', values: { DEEPSEEK_BASE_URL: 'https://user.example' } },
|
||||
])
|
||||
expect(resolveAdapterOptions({}, trusted).baseURL).toBe('https://user.example')
|
||||
// The product trusts the project it is launched in, so a checkout can
|
||||
// point its own agent at the gateway that checkout is meant to use.
|
||||
const project = createEnvironmentSnapshot([
|
||||
const project = createLaunchEnvironmentSnapshot([
|
||||
{ source: 'project-env', path: '/work/.env', values: { DEEPSEEK_BASE_URL: 'https://project.example' } },
|
||||
])
|
||||
expect(resolveAdapterOptions({}, project).baseURL).toBe('https://project.example')
|
||||
// An explicitly configured endpoint outranks every environment layer, so a
|
||||
// stale shell value cannot rewrite a deployment's own gateway.
|
||||
const shell = createEnvironmentSnapshot([
|
||||
const shell = createLaunchEnvironmentSnapshot([
|
||||
{ source: 'process', values: { DEEPSEEK_BASE_URL: 'https://stale.example' } },
|
||||
])
|
||||
expect(resolveAdapterOptions({ baseURL: 'https://gateway.internal' }, shell).baseURL).toBe('https://gateway.internal')
|
||||
@@ -995,7 +995,7 @@ describe('plugin registration and config', () => {
|
||||
vi.stubEnv('DEEPSEEK_API_KEY', 'k')
|
||||
vi.stubEnv('DEEPSEEK_BASE_URL', undefined)
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
// Registration succeeds; no call is made (would hit api.deepseek.com).
|
||||
await ctx.plugin(LlmDeepSeek, {})
|
||||
expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek-official', name: 'DeepSeek' }])
|
||||
@@ -1031,7 +1031,7 @@ describe('plugin registration and config', () => {
|
||||
.toThrow(/streamIdleTimeoutMs.*no greater/)
|
||||
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
await expect(ctx.plugin(LlmDeepSeek, {
|
||||
baseURL: 'http://127.0.0.1:1',
|
||||
streamIdleTimeoutMs: 0,
|
||||
@@ -1044,7 +1044,7 @@ describe('plugin registration and config', () => {
|
||||
|
||||
it('rejects invalid nested retryPolicy before registering the provider', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
|
||||
await expect(ctx.plugin(LlmDeepSeek, {
|
||||
baseURL: 'http://127.0.0.1:1',
|
||||
|
||||
@@ -3,11 +3,11 @@ import { Context } from '@deepseek-ai/cordis'
|
||||
import { access, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import LlmService, { INVALID_CREDENTIAL_CODE } from '@deepseek-ai/dsh-llm'
|
||||
import LlmRuntime, { INVALID_CREDENTIAL_CODE } from '@deepseek-ai/dsh-llm'
|
||||
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
||||
import { CredentialsLocal } from '@deepseek-ai/dsh-credentials-local'
|
||||
import { LocalCredentialProvider } from '@deepseek-ai/dsh-credentials-local'
|
||||
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
||||
import { SettingsLocal } from '@deepseek-ai/dsh-settings-local'
|
||||
import { FileSettingsProvider } from '@deepseek-ai/dsh-settings-file'
|
||||
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
|
||||
import { assemble } from './assemble.ts'
|
||||
import { closeMockServers, mockServer, textEvents } from './mock-server.ts'
|
||||
@@ -35,7 +35,7 @@ interface Harness {
|
||||
}
|
||||
|
||||
/**
|
||||
* Real dynamic composition: llm + settings-local + credentials-local +
|
||||
* Real dynamic composition: llm + settings-file + credentials-local +
|
||||
* llm-deepseek over one temp harness home. `watch: false` keeps every change
|
||||
* flowing through the in-process write path, which is deterministic; external
|
||||
* file watching is the providers' own covered concern.
|
||||
@@ -46,10 +46,10 @@ async function boot(dir: string, config: object): Promise<Harness> {
|
||||
cleanups.push(async () => {
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
await ctx.plugin(LlmService)
|
||||
const settingsFiber = ctx.plugin(SettingsLocal, { path: join(dir, 'settings.yaml'), watch: false })
|
||||
await ctx.plugin(LlmRuntime)
|
||||
const settingsFiber = ctx.plugin(FileSettingsProvider, { path: join(dir, 'settings.yaml'), watch: false })
|
||||
await settingsFiber
|
||||
await ctx.plugin(CredentialsLocal, { path: join(dir, '.credentials.yaml'), watch: false })
|
||||
await ctx.plugin(LocalCredentialProvider, { path: join(dir, '.credentials.yaml'), watch: false })
|
||||
await ctx.plugin(LlmDeepSeek, config)
|
||||
return { ctx, settingsFiber }
|
||||
}
|
||||
@@ -87,11 +87,11 @@ describe('request-level dynamic configuration', () => {
|
||||
|
||||
const keyless = await prompt(ctx)
|
||||
expect(keyless.finish).toMatchObject({ kind: 'error', failure: { code: 'MISSING_CREDENTIAL' } })
|
||||
await expect(access(join(dir, '.userid'))).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
await expect(access(join(dir, '.anonymous-user-id'))).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
await ctx.credentials.set(KEY_REF, 'sk-arrived')
|
||||
await prompt(ctx)
|
||||
expect(server.headers[0]?.authorization).toBe('Bearer sk-arrived')
|
||||
await expect(access(join(dir, '.userid'))).resolves.toBeUndefined()
|
||||
await expect(access(join(dir, '.anonymous-user-id'))).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
it('rejects a stored credential no header can carry, never echoing it in the failure', async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Real-composition guard for the dynamic-configuration chain: LlmService,
|
||||
* settings-local, credentials-local, and llm-deepseek boot from a test-only
|
||||
* Real-composition guard for the dynamic-configuration chain: LlmRuntime,
|
||||
* settings-file, credentials-local, and llm-deepseek boot from a test-only
|
||||
* cordis.yml through the actual Loader + Include path, external edits of
|
||||
* settings.yaml and the credentials document hot-publish through their providers, and the very
|
||||
* next request carries the fresh base URL and credential. The same adapter
|
||||
@@ -16,12 +16,12 @@ 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 LlmService from '@deepseek-ai/dsh-llm'
|
||||
import LlmRuntime from '@deepseek-ai/dsh-llm'
|
||||
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
||||
import CredentialsLocal from '@deepseek-ai/dsh-credentials-local'
|
||||
import LocalCredentialProvider from '@deepseek-ai/dsh-credentials-local'
|
||||
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
||||
import SettingsLocal from '@deepseek-ai/dsh-settings-local'
|
||||
import { getOrCreateAnonymousUserId } from '@deepseek-ai/dsh-user-id'
|
||||
import FileSettingsProvider from '@deepseek-ai/dsh-settings-file'
|
||||
import { getOrCreateAnonymousUserId } from '@deepseek-ai/dsh-anonymous-user-id'
|
||||
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
|
||||
import { assemble } from './assemble.ts'
|
||||
import { closeMockServers, mockServer, textEvents } from './mock-server.ts'
|
||||
@@ -63,7 +63,7 @@ async function loadComposition(
|
||||
...options.withDynamic
|
||||
? [
|
||||
'- id: settings',
|
||||
" name: '@deepseek-ai/dsh-settings-local'",
|
||||
" name: '@deepseek-ai/dsh-settings-file'",
|
||||
' config:',
|
||||
` path: ${JSON.stringify(settingsPath)}`,
|
||||
' debounceMs: 10',
|
||||
@@ -87,9 +87,9 @@ async function loadComposition(
|
||||
await ctx.plugin(Loader)
|
||||
ctx.loader.builtins.include = Include
|
||||
const modules = new Map<string, unknown>([
|
||||
['test-llm-service', LlmService],
|
||||
['@deepseek-ai/dsh-settings-local', SettingsLocal],
|
||||
['@deepseek-ai/dsh-credentials-local', CredentialsLocal],
|
||||
['test-llm-service', LlmRuntime],
|
||||
['@deepseek-ai/dsh-settings-file', FileSettingsProvider],
|
||||
['@deepseek-ai/dsh-credentials-local', LocalCredentialProvider],
|
||||
['@deepseek-ai/dsh-llm-deepseek', LlmDeepSeek],
|
||||
])
|
||||
ctx.loader.internal = {
|
||||
|
||||
@@ -24,19 +24,19 @@
|
||||
"path": "../../credentials/credentials"
|
||||
},
|
||||
{
|
||||
"path": "../../util/environment"
|
||||
"path": "../../util/launch-environment"
|
||||
},
|
||||
{
|
||||
"path": "../../settings/settings"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
"path": "../../runtime-diagnostics/invariants"
|
||||
},
|
||||
{
|
||||
"path": "../../util/timeout"
|
||||
},
|
||||
{
|
||||
"path": "../../session/user-id"
|
||||
"path": "../../identity/anonymous-user-id"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user