Merge remote-tracking branch 'origin/master' into worktree/pi-ai-per-model-reasoning
# Conflicts: # docs/config-catalog.md # docs/user/guide/providers.i18n.yaml # packages/llm/llm-pi-ai/README.i18n.yaml # packages/llm/llm-pi-ai/README.md # packages/llm/llm-pi-ai/README.zh.md # packages/llm/llm-pi-ai/tests/config.spec.ts
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import LlmService, { createUserMessage, CONTEXT_WINDOW_EXCEEDED_CODE, LlmError, ReasoningEffortId, userAgent } from '@deepseek-ai/dsh-llm'
|
||||
import * as LlmPiAi from '@deepseek-ai/dsh-llm-pi-ai'
|
||||
@@ -15,22 +15,32 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
async function harness(baseURL: string, overrides: Record<string, unknown> = {}): Promise<Context> {
|
||||
vi.stubEnv('PI_TEST_KEY', 'test-key')
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmPiAi, {
|
||||
providers: { deepseek: { apiKey: 'test-key', baseURL, ...overrides } },
|
||||
providers: { deepseek: { apiKeyEnv: 'PI_TEST_KEY', baseURL, ...overrides } },
|
||||
})
|
||||
return ctx
|
||||
}
|
||||
|
||||
/** Direct adapter over the real profile resolver, with literal-key resolution. */
|
||||
function adapterOf(providers: Record<string, LlmPiAi.PiAiProviderProfile>): PiAiAdapter {
|
||||
/** Direct adapter over the real profile resolver, with a fixed key per call. */
|
||||
function adapterOf(
|
||||
providers: Record<string, LlmPiAi.PiAiProviderProfile>,
|
||||
apiKey: string | undefined = 'test-key',
|
||||
): PiAiAdapter {
|
||||
return new PiAiAdapter({
|
||||
profiles: () => resolveProfiles(providers),
|
||||
resolveApiKey: (_provider, profile) => Promise.resolve(profile.apiKey),
|
||||
resolveApiKey: () => Promise.resolve(apiKey),
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
// Configuration carries only the reference; these mounts resolve it from
|
||||
// the environment, which is the whole credential plane without a seam.
|
||||
vi.stubEnv('PI_TEST_KEY', 'test-key')
|
||||
})
|
||||
|
||||
describe('PiAiAdapter provider routing', () => {
|
||||
it('resolves a catalog model dynamically and uses a private endpoint', async () => {
|
||||
const server = await mockServer([{ events: textEvents }])
|
||||
@@ -121,7 +131,7 @@ describe('PiAiAdapter provider routing', () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
ctx.llm.registerAdapter(['deepseek'], adapterOf({
|
||||
deepseek: { apiKey: 'test-key', baseURL: server.url },
|
||||
deepseek: { apiKeyEnv: 'PI_TEST_KEY', baseURL: server.url },
|
||||
}))
|
||||
|
||||
const result = await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] })
|
||||
@@ -131,7 +141,6 @@ describe('PiAiAdapter provider routing', () => {
|
||||
|
||||
it('names a route by its displayName, and by its own key once the profiles drop it', () => {
|
||||
const adapter = adapterOf({ 'acme-gateway': {
|
||||
apiKey: 'k',
|
||||
displayName: 'Acme Gateway',
|
||||
api: 'openai-completions',
|
||||
baseURL: 'https://acme.test/v1',
|
||||
@@ -167,7 +176,7 @@ describe('PiAiAdapter provider routing', () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmPiAi, {
|
||||
providers: { openai: { apiKey: 'test-key', baseURL: `${server.url}/v1` } },
|
||||
providers: { openai: { apiKeyEnv: 'PI_TEST_KEY', baseURL: `${server.url}/v1` } },
|
||||
})
|
||||
const result = await assemble(ctx, { provider: 'openai', model: 'gpt-4.1', messages: [] })
|
||||
expect(result.finish.kind).toBe('error')
|
||||
@@ -187,7 +196,7 @@ describe('PiAiAdapter provider routing', () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmPiAi, {
|
||||
providers: { openai: { apiKey: 'test-key', baseURL: `${server.url}/v1` } },
|
||||
providers: { openai: { apiKeyEnv: 'PI_TEST_KEY', baseURL: `${server.url}/v1` } },
|
||||
})
|
||||
|
||||
const result = await assemble(ctx, { provider: 'openai', model: 'gpt-4.1', messages: [] })
|
||||
@@ -203,7 +212,7 @@ describe('PiAiAdapter provider routing', () => {
|
||||
await ctx.plugin(LlmPiAi, {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: 'test-key',
|
||||
apiKeyEnv: 'PI_TEST_KEY',
|
||||
baseURL: `${server.url}/api/projects/openai/openai/v1`,
|
||||
headers: { 'api-key': 'test-key', Authorization: '' },
|
||||
},
|
||||
@@ -406,7 +415,7 @@ describe('provider profile lifecycle', () => {
|
||||
await ctx.plugin(LlmPiAi, {
|
||||
providers: {
|
||||
'acme-gateway': {
|
||||
apiKey: 'gw-key',
|
||||
apiKeyEnv: 'PI_TEST_KEY',
|
||||
api: 'openai-completions',
|
||||
baseURL: 'https://acme.test/v1',
|
||||
reasoning: 'high',
|
||||
@@ -435,13 +444,14 @@ describe('provider profile lifecycle', () => {
|
||||
})
|
||||
|
||||
it('sends the declared wire spelling and refuses undeclared levels before network I/O', async () => {
|
||||
vi.stubEnv('PI_TEST_KEY', 'test-key')
|
||||
const server = await mockServer([{ events: textEvents }])
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmPiAi, {
|
||||
providers: {
|
||||
'acme-gateway': {
|
||||
apiKey: 'gw-key',
|
||||
apiKeyEnv: 'PI_TEST_KEY',
|
||||
api: 'openai-completions',
|
||||
baseURL: `${server.url}/v1`,
|
||||
models: [{
|
||||
@@ -477,13 +487,14 @@ describe('provider profile lifecycle', () => {
|
||||
})
|
||||
|
||||
it('dispatches the compat-switched dialect on a declared route', async () => {
|
||||
vi.stubEnv('PI_TEST_KEY', 'test-key')
|
||||
const server = await mockServer([{ events: textEvents }, { events: textEvents }])
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmPiAi, {
|
||||
providers: {
|
||||
'acme-gateway': {
|
||||
apiKey: 'gw-key',
|
||||
apiKeyEnv: 'PI_TEST_KEY',
|
||||
api: 'openai-completions',
|
||||
baseURL: `${server.url}/v1`,
|
||||
// Without the switch pi-ai guesses the dialect from the endpoint
|
||||
@@ -514,13 +525,14 @@ describe('provider profile lifecycle', () => {
|
||||
})
|
||||
|
||||
it('holds back reasoning_effort when the endpoint cannot take it', async () => {
|
||||
vi.stubEnv('PI_TEST_KEY', 'test-key')
|
||||
const server = await mockServer([{ events: textEvents }])
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmPiAi, {
|
||||
providers: {
|
||||
'acme-gateway': {
|
||||
apiKey: 'gw-key',
|
||||
apiKeyEnv: 'PI_TEST_KEY',
|
||||
api: 'openai-completions',
|
||||
baseURL: `${server.url}/v1`,
|
||||
compat: { supportsReasoningEffort: false },
|
||||
@@ -546,7 +558,9 @@ describe('provider profile lifecycle', () => {
|
||||
it('accepts absent credentials for pi-ai ambient authentication', async () => {
|
||||
vi.stubEnv('DEEPSEEK_API_KEY', 'ambient-key')
|
||||
const server = await mockServer([{ events: textEvents }])
|
||||
const ctx = await harness(server.url, { apiKey: undefined })
|
||||
// A profile that names no reference at all is the one case that defers to
|
||||
// pi-ai's own provider-native discovery.
|
||||
const ctx = await harness(server.url, { apiKeyEnv: undefined })
|
||||
await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] })
|
||||
expect(server.headers[0]?.authorization).toBe('Bearer ambient-key')
|
||||
})
|
||||
@@ -588,8 +602,6 @@ describe('provider profile lifecycle', () => {
|
||||
// loud with migration directions instead of half-working.
|
||||
expect(() => resolveProfiles([{ provider: 'openai' }] as never)).toThrow(/dict keyed by provider/)
|
||||
expect(() => resolveProfiles({ openai: { provider: 'openai' } as never })).toThrow(/moved to the providers dict key/)
|
||||
expect(() => resolveProfiles({ openai: { apiKey: '' } })).toThrow(/empty apiKey/)
|
||||
expect(() => resolveProfiles({ openai: { apiKey: ' ' } })).toThrow(/empty apiKey/)
|
||||
expect(() => resolveProfiles({ openai: { baseURL: '' } })).toThrow(/empty baseURL/)
|
||||
expect(() => resolveProfiles({ openai: { apiKeyEnv: 'not-a-var!' } })).toThrow(/must match/)
|
||||
})
|
||||
@@ -664,7 +676,7 @@ describe('abort wiring', () => {
|
||||
const message = Object.defineProperty({}, 'role', {
|
||||
get() { throw original },
|
||||
})
|
||||
const adapter = adapterOf({ deepseek: { apiKey: 'test-key' } })
|
||||
const adapter = adapterOf({ deepseek: {} })
|
||||
const drain = async (): Promise<void> => {
|
||||
for await (const _chunk of adapter.stream({
|
||||
provider: 'deepseek',
|
||||
@@ -685,7 +697,7 @@ describe('abort wiring', () => {
|
||||
throw original
|
||||
},
|
||||
})
|
||||
const adapter = adapterOf({ deepseek: { apiKey: 'test-key' } })
|
||||
const adapter = adapterOf({ deepseek: {} })
|
||||
const drain = async (): Promise<void> => {
|
||||
for await (const _chunk of adapter.stream({
|
||||
provider: 'deepseek',
|
||||
@@ -699,7 +711,7 @@ describe('abort wiring', () => {
|
||||
})
|
||||
|
||||
it('resolves catalog endpoints without an override before honoring pre-abort', async () => {
|
||||
const adapter = adapterOf({ deepseek: { apiKey: 'test-key' } })
|
||||
const adapter = adapterOf({ deepseek: {} })
|
||||
const controller = new AbortController()
|
||||
controller.abort('already stopped')
|
||||
const chunks = []
|
||||
|
||||
Reference in New Issue
Block a user