|
|
|
|
@@ -61,89 +61,6 @@ export function resolveLanTrust(
|
|
|
|
|
return { lanAddresses, trustedHosts: [...lanAddresses, ...extra] }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** One provider/model source layer for {@link resolveLlmRoute}, in override order. */
|
|
|
|
|
export interface LlmRouteInput {
|
|
|
|
|
/** CLI flag values (highest precedence). */
|
|
|
|
|
cli: { provider?: string | undefined; model?: string | undefined }
|
|
|
|
|
/** Profile-json values (parsed JSON — validated here, the config boundary). */
|
|
|
|
|
profile: { provider?: unknown; model?: unknown }
|
|
|
|
|
/** The api-gateway yml row's config values (deployment defaults). */
|
|
|
|
|
gateway: { provider?: unknown; model?: unknown }
|
|
|
|
|
/** Providers the shipped yml already routes through its static pi-ai row. */
|
|
|
|
|
ymlPiAiProviders: readonly string[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** The boot's resolved LLM routing decision. */
|
|
|
|
|
export interface LlmRoute {
|
|
|
|
|
/** Effective api-gateway provider. */
|
|
|
|
|
provider: string
|
|
|
|
|
/** Pi-ai provider to mount dynamically; undefined when DeepSeek or a yml-routed provider serves the request. */
|
|
|
|
|
dynamicPiAiProvider: string | undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Resolve the boot's LLM route from the layered provider/model sources.
|
|
|
|
|
* A non-DeepSeek provider requires a model set at least as explicitly as the
|
|
|
|
|
* provider itself (flag/profile) — origin decides, never a comparison against
|
|
|
|
|
* any deployment's default model value, so editing the yml default cannot
|
|
|
|
|
* silently disarm the guard. Providers the shipped yml pi-ai row already
|
|
|
|
|
* routes are NOT mounted again: `LlmService.registerAdapter` rejects
|
|
|
|
|
* duplicate routes, so the gateway provider/model patch alone selects them.
|
|
|
|
|
* @param input - the layered provider/model sources and the yml pi-ai roster.
|
|
|
|
|
* @returns the effective provider and the dynamic pi-ai mount decision.
|
|
|
|
|
*/
|
|
|
|
|
export function resolveLlmRoute(input: LlmRouteInput): LlmRoute {
|
|
|
|
|
const provider = input.cli.provider ?? input.profile.provider ?? input.gateway.provider
|
|
|
|
|
if (typeof provider !== 'string' || provider === '') {
|
|
|
|
|
throw new Error('dsh: api-gateway provider must be a non-empty string')
|
|
|
|
|
}
|
|
|
|
|
if (provider !== 'deepseek') {
|
|
|
|
|
const providerFromYml = input.cli.provider === undefined && input.profile.provider === undefined
|
|
|
|
|
// A yml-set provider trusts its own row pairing; an override must bring
|
|
|
|
|
// its model along instead of inheriting the yml default's.
|
|
|
|
|
const model = providerFromYml
|
|
|
|
|
? input.gateway.model
|
|
|
|
|
: input.cli.model ?? input.profile.model
|
|
|
|
|
if (typeof model !== 'string' || model === '') {
|
|
|
|
|
throw new Error(`dsh: provider ${provider} requires an explicit model`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
provider,
|
|
|
|
|
dynamicPiAiProvider: provider === 'deepseek' || input.ymlPiAiProviders.includes(provider)
|
|
|
|
|
? undefined
|
|
|
|
|
: provider,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Bypass parse of an include yml's top-level entry rows (id → row). Exported
|
|
|
|
|
* so tests can pin the shipped tree's real row coupling instead of literals.
|
|
|
|
|
* @param configPath - absolute path of the include cordis.yml.
|
|
|
|
|
* @returns row map keyed by entry id.
|
|
|
|
|
*/
|
|
|
|
|
export function parseIncludeYmlRows(configPath: string): Map<string, { config?: unknown }> {
|
|
|
|
|
const doc = yaml.load(readFileSync(configPath, 'utf8'), { schema: includeYamlSchema })
|
|
|
|
|
if (!Array.isArray(doc)) throw new Error(`dsh: ${configPath} is not a top-level entry list`)
|
|
|
|
|
const rows = new Map<string, { config?: unknown }>()
|
|
|
|
|
for (const row of doc as { id?: string; config?: unknown }[]) {
|
|
|
|
|
if (typeof row.id === 'string') rows.set(row.id, row)
|
|
|
|
|
}
|
|
|
|
|
return rows
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Providers the yml's static pi-ai row routes — the roster {@link resolveLlmRoute} reuses.
|
|
|
|
|
* @param rows - parsed include rows.
|
|
|
|
|
* @returns provider ids in row order (empty when the row is absent).
|
|
|
|
|
*/
|
|
|
|
|
export function ymlPiAiProvidersOf(rows: ReadonlyMap<string, { config?: unknown }>): string[] {
|
|
|
|
|
const config = rows.get('llm-pi-ai')?.config as { providers?: { provider?: unknown }[] } | undefined
|
|
|
|
|
return (config?.providers ?? [])
|
|
|
|
|
.map(entry => entry.provider)
|
|
|
|
|
.filter((value): value is string => typeof value === 'string')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** One profile-json key mapped onto a yml row's config field. */
|
|
|
|
|
interface ProfileMapping {
|
|
|
|
|
jsonPath: string
|
|
|
|
|
@@ -198,14 +115,6 @@ export interface AppCLIEntryOptions {
|
|
|
|
|
port?: number
|
|
|
|
|
/** Parent directory for name-created Workspaces; undefined uses the gateway's cwd fallback. */
|
|
|
|
|
workspaceRoot?: string
|
|
|
|
|
/**
|
|
|
|
|
* Host default provider override. Providers the shipped yml pi-ai row
|
|
|
|
|
* already routes are reused; only a provider absent from that row mounts
|
|
|
|
|
* pi-ai dynamically.
|
|
|
|
|
*/
|
|
|
|
|
provider?: string
|
|
|
|
|
/** Host default model override. */
|
|
|
|
|
model?: string
|
|
|
|
|
/** Extra authorities for the /api browser-trust fence (`host` or `host:port`), appended to the derived LAN IP literals. */
|
|
|
|
|
trustedHosts?: string[]
|
|
|
|
|
}
|
|
|
|
|
@@ -229,7 +138,6 @@ export class AppCLIEntry {
|
|
|
|
|
lanAddresses: readonly string[] = []
|
|
|
|
|
|
|
|
|
|
private patches: PatchOptions[] = []
|
|
|
|
|
private piAiProvider: string | undefined
|
|
|
|
|
|
|
|
|
|
constructor(private readonly options: AppCLIEntryOptions) {}
|
|
|
|
|
|
|
|
|
|
@@ -290,17 +198,6 @@ export class AppCLIEntry {
|
|
|
|
|
if (this.options.host !== undefined) put('webserver', 'host', this.options.host)
|
|
|
|
|
if (this.options.port !== undefined) put('webserver', 'port', this.options.port)
|
|
|
|
|
if (this.options.workspaceRoot !== undefined) put('api-gateway', 'workspaceRoot', this.options.workspaceRoot)
|
|
|
|
|
if (this.options.provider !== undefined) put('api-gateway', 'provider', this.options.provider)
|
|
|
|
|
if (this.options.model !== undefined) put('api-gateway', 'model', this.options.model)
|
|
|
|
|
|
|
|
|
|
const gatewayConfig = rows.get('api-gateway')?.config as Record<string, unknown> | undefined
|
|
|
|
|
const route = resolveLlmRoute({
|
|
|
|
|
cli: { provider: this.options.provider, model: this.options.model },
|
|
|
|
|
profile: { provider: profile.provider, model: profile.model },
|
|
|
|
|
gateway: { provider: gatewayConfig?.provider, model: gatewayConfig?.model },
|
|
|
|
|
ymlPiAiProviders: ymlPiAiProvidersOf(rows),
|
|
|
|
|
})
|
|
|
|
|
this.piAiProvider = route.dynamicPiAiProvider
|
|
|
|
|
|
|
|
|
|
// Source 2b: authorities for the /api browser-trust fence (rationale on
|
|
|
|
|
// resolveLanTrust).
|
|
|
|
|
@@ -333,12 +230,6 @@ export class AppCLIEntry {
|
|
|
|
|
...this.patches.length > 0 ? { patches: this.patches } : {},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
if (this.piAiProvider !== undefined) {
|
|
|
|
|
await ctx.loader.create({
|
|
|
|
|
name: '@deepseek-ai/dsh-llm-pi-ai',
|
|
|
|
|
config: { providers: [{ provider: this.piAiProvider }] },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
if (this.options.dev) {
|
|
|
|
|
await ctx.loader.create({ name: '@deepseek-ai/dsh-client-hmr' })
|
|
|
|
|
}
|
|
|
|
|
@@ -373,7 +264,13 @@ export class AppCLIEntry {
|
|
|
|
|
|
|
|
|
|
/** Bypass parse of the shipped yml (id → row) for patch-merge inputs; Loader still reads the file itself. */
|
|
|
|
|
private parseYmlRows(): Map<string, { config?: unknown }> {
|
|
|
|
|
return parseIncludeYmlRows(this.options.configPath)
|
|
|
|
|
const doc = yaml.load(readFileSync(this.options.configPath, 'utf8'), { schema: includeYamlSchema })
|
|
|
|
|
if (!Array.isArray(doc)) throw new Error(`dsh: ${this.options.configPath} is not a top-level entry list`)
|
|
|
|
|
const rows = new Map<string, { config?: unknown }>()
|
|
|
|
|
for (const row of doc as { id?: string; config?: unknown }[]) {
|
|
|
|
|
if (typeof row.id === 'string') rows.set(row.id, row)
|
|
|
|
|
}
|
|
|
|
|
return rows
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Profile json under cwd; read-only — never created here, absent = no user config. */
|
|
|
|
|
|