Merge branch 'worktree-llm-dynamic-config' into worktree-llm-web-config
# Conflicts: # docs/cordis-catalog/services.md # docs/core-data-structures/core.i18n.yaml # docs/event-producer-consumer.md # examples/headless-agent/tests/headless.snapshot.ts # examples/headless-agent/tests/snapshots/missing-credential/stream-json.expected.jsonl # packages/llm/llm-deepseek/README.i18n.yaml # packages/llm/llm-deepseek/src/index.ts # packages/llm/llm-pi-ai/README.i18n.yaml # packages/llm/llm-pi-ai/src/index.ts # packages/llm/llm/README.i18n.yaml # packages/llm/llm/src/index.ts
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/README.md
|
||||
README.md: 12bf3ca9901a7027111761a6d523862f9f7e150b
|
||||
README.zh.md: d39b6c518f332e0778e10b73bdcf1ddd25433ab3
|
||||
README.md: b338f4d07ae4c5e3dd9a04ad0765fb8b6d6a99a7
|
||||
README.zh.md: 11d4c92573e2bdba74d1f7e04b445a9039824267
|
||||
|
||||
@@ -10,7 +10,7 @@ An adapter registry plus a single streaming call surface, interceptable via a wa
|
||||
|
||||
### Public API
|
||||
|
||||
- `ctx.llm.registerAdapter(providers: string[], adapter: LlmAdapter): () => void` Register one adapter instance for the given provider routes. Registration is all-or-nothing, and is disposed with the calling fiber.
|
||||
- `ctx.llm.registerAdapter(providers: string[], adapter: LlmAdapter): AdapterRegistrationHandle` Register one adapter instance for the given provider routes. Registration is all-or-nothing, and is disposed with the calling fiber. The returned disposer also carries `replace(providers)`: the candidate route set is validated in full before anything moves, so a conflict with another adapter leaves the current routes registered and serving, and the swap itself is one synchronous section with no observable gap. `replace([])` is legal — a registration holding zero routes — unlike an empty initial registration.
|
||||
- `ctx.llm.listProviders(): LlmProviderInfo[]` Describe registered provider routes in registration order.
|
||||
- `ctx.llm.registerConfigurableProviders(entries: readonly LlmConfigurableProvider[]): () => void` Declare provider routes an adapter plugin can activate through configuration — registered or dormant — each naming its owning settings namespace and the path to its profile inside that section. All-or-nothing (`INVALID_DIRECTORY`/`DUPLICATE_DIRECTORY`), disposed with the calling fiber.
|
||||
- `ctx.llm.listConfigurableProviders(): LlmConfigurableProvider[]` List the declared directory in declaration order; configuration surfaces merge it with `listProviders()` to mark each entry live or dormant.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
### 公开 API
|
||||
|
||||
- `ctx.llm.registerAdapter(providers: string[], adapter: LlmAdapter): () => void` 为给定提供方路由注册一个适配器实例。注册要么全部成功,要么全部不生效,并且会随调用 fiber 一起 dispose(资源释放)。
|
||||
- `ctx.llm.registerAdapter(providers: string[], adapter: LlmAdapter): AdapterRegistrationHandle` 为给定提供方路由注册一个适配器实例。注册要么全部成功,要么全部不生效,并且会随调用 fiber 一起 dispose(资源释放)。返回的释放器还携带 `replace(providers)`:候选路由集合会在任何东西变动之前完整校验,因此与另一适配器冲突时,当前路由保持注册且继续服务,而替换本身是一个同步区段,不存在可观察的空档。`replace([])` 合法——一个持有零条路由的注册——这与空的初始注册不同。
|
||||
- `ctx.llm.listProviders(): LlmProviderInfo[]` 按注册顺序描述已注册提供方路由。
|
||||
- `ctx.llm.registerConfigurableProviders(entries: readonly LlmConfigurableProvider[]): () => void` 声明适配器插件可通过配置激活的提供方路由——无论已注册还是休眠——每个条目指明其所属 settings namespace,以及 profile 在该分节内的路径。要么全部成功,要么全部不生效(`INVALID_DIRECTORY`/`DUPLICATE_DIRECTORY`),并随调用 fiber dispose。
|
||||
- `ctx.llm.listConfigurableProviders(): LlmConfigurableProvider[]` 按声明顺序列出已声明的目录;配置界面将其与 `listProviders()` 合并,为每个条目标注存活或休眠。
|
||||
|
||||
@@ -196,6 +196,26 @@ export abstract class LlmAdapter {
|
||||
abstract stream(options: GenerateOptions): AsyncIterable<StreamChunk>
|
||||
}
|
||||
|
||||
/**
|
||||
* What {@link LlmService.registerAdapter} returns: the disposer, plus an
|
||||
* atomic route replacement for the same adapter instance.
|
||||
*/
|
||||
export interface AdapterRegistrationHandle {
|
||||
/** Release every route this registration currently holds. */
|
||||
(): void
|
||||
/**
|
||||
* Replace this registration's routes with `providers`, keeping the same
|
||||
* adapter instance. The candidate set is validated in full first — a
|
||||
* conflict with another adapter, an invalid name, or bad provider metadata
|
||||
* throws and leaves the current routes untouched — and the swap itself is
|
||||
* one synchronous section, so no request can observe a gap. An empty array
|
||||
* is legal here (a settings section that emptied holds zero routes while
|
||||
* staying registered), unlike an empty initial registration.
|
||||
* @param providers - the complete next route set for this registration.
|
||||
*/
|
||||
replace(providers: string[]): void
|
||||
}
|
||||
|
||||
/**
|
||||
* The abstract `llm` service: an adapter registry plus a streaming model-call
|
||||
* surface, interceptable via the `llm/stream` waterfall.
|
||||
@@ -235,41 +255,74 @@ export class LlmService extends Service {
|
||||
* Disposed with the fiber.
|
||||
* @param providers - every provider route this adapter should serve.
|
||||
* @param adapter - the adapter that streams calls for those providers.
|
||||
* @returns the disposer that unregisters all of them.
|
||||
* @returns the disposer, carrying {@link AdapterRegistrationHandle.replace}.
|
||||
*/
|
||||
registerAdapter(providers: string[], adapter: LlmAdapter): () => void {
|
||||
registerAdapter(providers: string[], adapter: LlmAdapter): AdapterRegistrationHandle {
|
||||
// The routes this registration currently holds; `replace` rewrites it, and
|
||||
// the disposer releases whatever it holds at disposal time.
|
||||
const owned = new Set<string>()
|
||||
const dispose = this.ctx.effect(function* (this: LlmService) {
|
||||
if (providers.length === 0) throw new LlmError('an adapter must register at least one provider', 'INVALID_ADAPTER')
|
||||
const unique = new Set<string>()
|
||||
const registrations: AdapterRegistration[] = []
|
||||
for (const provider of providers) {
|
||||
if (provider.length === 0) throw new LlmError('adapter provider names must be non-empty', 'INVALID_ADAPTER')
|
||||
if (unique.has(provider) || this.adapters.has(provider)) {
|
||||
throw new LlmError(`an adapter for provider "${provider}" is already registered`, 'DUPLICATE_ADAPTER')
|
||||
}
|
||||
const info = adapter.providerInfo(provider)
|
||||
if (typeof info.id !== 'string' || info.id !== provider || typeof info.name !== 'string' || info.name.length === 0) {
|
||||
throw new LlmError(`adapter metadata for provider "${provider}" must preserve its id and have a non-empty name`, 'INVALID_ADAPTER')
|
||||
}
|
||||
unique.add(provider)
|
||||
const retryPolicy = adapter.providerRetryPolicy(provider)
|
||||
?? resolveRetryPolicy(undefined, `llm: provider "${provider}" retryPolicy`)
|
||||
registrations.push({
|
||||
adapter,
|
||||
provider: { id: info.id, name: info.name },
|
||||
retryPolicy,
|
||||
})
|
||||
}
|
||||
for (const registration of registrations) this.adapters.set(registration.provider.id, registration)
|
||||
this.emitAdaptersUpdated()
|
||||
this.commitRoutes(owned, this.prepareRoutes(providers, adapter, owned))
|
||||
yield () => {
|
||||
for (const provider of providers) this.adapters.delete(provider)
|
||||
for (const provider of owned) this.adapters.delete(provider)
|
||||
owned.clear()
|
||||
this.emitAdaptersUpdated()
|
||||
}
|
||||
}.bind(this), 'llm.registerAdapter()')
|
||||
// ctx.effect's disposer returns Promise<void>; our disposer API is
|
||||
// synchronous fire-and-forget — discard the (always-resolved) promise.
|
||||
return () => void dispose()
|
||||
const handle = (() => void dispose()) as AdapterRegistrationHandle
|
||||
handle.replace = (next: string[]): void => {
|
||||
this.commitRoutes(owned, this.prepareRoutes(next, adapter, owned))
|
||||
}
|
||||
return handle
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate one candidate route set for `adapter`, treating routes this
|
||||
* registration already holds as available. Nothing is mutated: a rejected
|
||||
* candidate leaves the registry exactly as it was.
|
||||
*/
|
||||
private prepareRoutes(providers: string[], adapter: LlmAdapter, owned: ReadonlySet<string>): AdapterRegistration[] {
|
||||
const unique = new Set<string>()
|
||||
const registrations: AdapterRegistration[] = []
|
||||
for (const provider of providers) {
|
||||
if (provider.length === 0) throw new LlmError('adapter provider names must be non-empty', 'INVALID_ADAPTER')
|
||||
if (unique.has(provider) || (this.adapters.has(provider) && !owned.has(provider))) {
|
||||
throw new LlmError(`an adapter for provider "${provider}" is already registered`, 'DUPLICATE_ADAPTER')
|
||||
}
|
||||
const info = adapter.providerInfo(provider)
|
||||
if (typeof info.id !== 'string' || info.id !== provider || typeof info.name !== 'string' || info.name.length === 0) {
|
||||
throw new LlmError(`adapter metadata for provider "${provider}" must preserve its id and have a non-empty name`, 'INVALID_ADAPTER')
|
||||
}
|
||||
unique.add(provider)
|
||||
const retryPolicy = adapter.providerRetryPolicy(provider)
|
||||
?? resolveRetryPolicy(undefined, `llm: provider "${provider}" retryPolicy`)
|
||||
registrations.push({
|
||||
adapter,
|
||||
provider: { id: info.id, name: info.name },
|
||||
retryPolicy,
|
||||
})
|
||||
}
|
||||
return registrations
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap this registration's routes for the prepared ones in one synchronous
|
||||
* section, so no observer can see the registry between the release and the
|
||||
* re-registration. The route set's one mutation point is also where
|
||||
* `llm/adapters-updated` is published, so a `replace` announces itself
|
||||
* exactly like a first registration.
|
||||
*/
|
||||
private commitRoutes(owned: Set<string>, registrations: readonly AdapterRegistration[]): void {
|
||||
for (const provider of owned) this.adapters.delete(provider)
|
||||
owned.clear()
|
||||
for (const registration of registrations) {
|
||||
this.adapters.set(registration.provider.id, registration)
|
||||
owned.add(registration.provider.id)
|
||||
}
|
||||
this.emitAdaptersUpdated()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user